Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 25.1.0
hooks:
- id: black
language_version: python3.13.3
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"**/.DS_Store": true
},
"[python]": {
"editor.defaultFormatter": "ms-python.autopep8",
"editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true
},
"sonarlint.connectedMode.project": {
Expand Down
10 changes: 9 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,17 @@ We enforce quality via CI on every push and PR:

Failures must be fixed before review.

## 6. Code of Conduct & Support
## 6. Formatting

We use [Black](https://black.readthedocs.io/) as the standard Python formatter in this project:

- All Python code must be formatted with Black before committing or pushing
- You can install the pre-commit hook to automatically format your code before each commit

## 7. Code of Conduct & Support

- Please see `CODE_OF_CONDUCT.md` for behavioral expectations and reporting.
- For quick questions or discussions, open an issue with the `discussion` label or mention a maintainer.

Thanks again for helping keep this project small, simple, and impactful!

10 changes: 3 additions & 7 deletions databases/player_database.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

The `STORAGE_PATH` environment variable controls the SQLite file location.
"""

import logging
import os
from typing import AsyncGenerator
Expand All @@ -21,16 +22,11 @@
logging.getLogger("sqlalchemy.engine.Engine").handlers = logger.handlers

async_engine = create_async_engine(
DATABASE_URL,
connect_args={"check_same_thread": False},
echo=True
DATABASE_URL, connect_args={"check_same_thread": False}, echo=True
)

async_sessionmaker = sessionmaker(
bind=async_engine,
class_=AsyncSession,
autocommit=False,
autoflush=False
bind=async_engine, class_=AsyncSession, autocommit=False, autoflush=False
)

Base = declarative_base()
Expand Down
13 changes: 9 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

This serves as the entry point for running the API server.
"""

from contextlib import asynccontextmanager
import logging
from typing import AsyncIterator
Expand All @@ -17,6 +18,7 @@
UVICORN_LOGGER = "uvicorn.error"
logger = logging.getLogger(UVICORN_LOGGER)


@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
"""
Expand All @@ -25,10 +27,13 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
logger.info("Lifespan event handler execution complete.")
yield

app = FastAPI(lifespan=lifespan,
title="python-samples-fastapi-restful",
description="🧪 Proof of Concept for a RESTful API made with Python 3 and FastAPI",
version="1.0.0",)

app = FastAPI(
lifespan=lifespan,
title="python-samples-fastapi-restful",
description="🧪 Proof of Concept for a RESTful API made with Python 3 and FastAPI",
version="1.0.0",
)

app.include_router(player_route.api_router)
app.include_router(health_route.api_router)
3 changes: 3 additions & 0 deletions models/player_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

These models are used for data validation and serialization in the API.
"""

from typing import Optional
from pydantic import BaseModel, ConfigDict
from pydantic.alias_generators import to_camel
Expand All @@ -24,6 +25,7 @@ class MainModel(BaseModel):
Here, it uses `to_camel` to convert field names to camelCase.
populate_by_name (bool): Allows population of fields by name when using Pydantic models.
"""

model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True)


Expand All @@ -44,6 +46,7 @@ class PlayerModel(MainModel):
league (Optional[str]): The league where the team plays, if any.
starting11 (Optional[bool]): Indicates if the Player is in the starting 11, if provided.
"""

id: int
first_name: str
middle_name: Optional[str]
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[tool.black]
line-length = 88
target-version = ['py312']
1 change: 1 addition & 0 deletions requirements-lint.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
flake8==7.2.0
black==25.1.0
1 change: 1 addition & 0 deletions requirements-pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pre-commit==4.2.0
2 changes: 2 additions & 0 deletions routes/health_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
Defines a simple endpoint to verify that the service is up and running.
Returns a JSON response with a "status" key set to "ok".
"""

from fastapi import APIRouter

api_router = APIRouter()


@api_router.get("/health", tags=["Health"])
async def health_check():
"""
Expand Down
35 changes: 20 additions & 15 deletions routes/player_route.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- PUT /players/{player_id} : Update an existing Player.
- DELETE /players/{player_id} : Delete an existing Player.
"""

from typing import List
from fastapi import APIRouter, Body, Depends, HTTPException, status, Path, Response
from sqlalchemy.ext.asyncio import AsyncSession
Expand All @@ -29,7 +30,7 @@
simple_memory_cache = SimpleMemoryCache()

CACHE_KEY = "players"
CACHE_TTL = 600 # 10 minutes
CACHE_TTL = 600 # 10 minutes

# POST -------------------------------------------------------------------------

Expand All @@ -38,11 +39,11 @@
"/players/",
status_code=status.HTTP_201_CREATED,
summary="Creates a new Player",
tags=["Players"]
tags=["Players"],
)
async def post_async(
player_model: PlayerModel = Body(...),
async_session: AsyncSession = Depends(generate_async_session)
async_session: AsyncSession = Depends(generate_async_session),
):
"""
Endpoint to create a new player.
Expand All @@ -60,6 +61,7 @@ async def post_async(
await player_service.create_async(async_session, player_model)
await simple_memory_cache.clear(CACHE_KEY)


# GET --------------------------------------------------------------------------


Expand All @@ -68,11 +70,10 @@ async def post_async(
response_model=List[PlayerModel],
status_code=status.HTTP_200_OK,
summary="Retrieves a collection of Players",
tags=["Players"]
tags=["Players"],
)
async def get_all_async(
response: Response,
async_session: AsyncSession = Depends(generate_async_session)
response: Response, async_session: AsyncSession = Depends(generate_async_session)
):
"""
Endpoint to retrieve all players.
Expand All @@ -97,11 +98,11 @@ async def get_all_async(
response_model=PlayerModel,
status_code=status.HTTP_200_OK,
summary="Retrieves a Player by its Id",
tags=["Players"]
tags=["Players"],
)
async def get_by_id_async(
player_id: int = Path(..., title="The ID of the Player"),
async_session: AsyncSession = Depends(generate_async_session)
async_session: AsyncSession = Depends(generate_async_session),
):
"""
Endpoint to retrieve a Player by its ID.
Expand All @@ -127,11 +128,11 @@ async def get_by_id_async(
response_model=PlayerModel,
status_code=status.HTTP_200_OK,
summary="Retrieves a Player by its Squad Number",
tags=["Players"]
tags=["Players"],
)
async def get_by_squad_number_async(
squad_number: int = Path(..., title="The Squad Number of the Player"),
async_session: AsyncSession = Depends(generate_async_session)
async_session: AsyncSession = Depends(generate_async_session),
):
"""
Endpoint to retrieve a Player by its Squad Number.
Expand All @@ -146,24 +147,27 @@ async def get_by_squad_number_async(
Raises:
HTTPException: HTTP 404 Not Found error if the Player with the specified Squad Number does not exist.
"""
player = await player_service.retrieve_by_squad_number_async(async_session, squad_number)
player = await player_service.retrieve_by_squad_number_async(
async_session, squad_number
)
if not player:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return player


# PUT --------------------------------------------------------------------------


@api_router.put(
"/players/{player_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Updates an existing Player",
tags=["Players"]
tags=["Players"],
)
async def put_async(
player_id: int = Path(..., title="The ID of the Player"),
player_model: PlayerModel = Body(...),
async_session: AsyncSession = Depends(generate_async_session)
async_session: AsyncSession = Depends(generate_async_session),
):
"""
Endpoint to entirely update an existing Player.
Expand All @@ -182,18 +186,19 @@ async def put_async(
await player_service.update_async(async_session, player_model)
await simple_memory_cache.clear(CACHE_KEY)


# DELETE -----------------------------------------------------------------------


@api_router.delete(
"/players/{player_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Deletes an existing Player",
tags=["Players"]
tags=["Players"],
)
async def delete_async(
player_id: int = Path(..., title="The ID of the Player"),
async_session: AsyncSession = Depends(generate_async_session)
async_session: AsyncSession = Depends(generate_async_session),
):
"""
Endpoint to delete an existing Player.
Expand Down
11 changes: 6 additions & 5 deletions schemas/player_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

Used for async database CRUD operations in the application.
"""

from sqlalchemy import Column, String, Integer, Boolean
from databases.player_database import Base

Expand All @@ -30,13 +31,13 @@ class Player(Base):
__tablename__ = "players"

id = Column(Integer, primary_key=True)
first_name = Column(String, name='firstName', nullable=False)
middle_name = Column(String, name='middleName')
last_name = Column(String, name='lastName', nullable=False)
first_name = Column(String, name="firstName", nullable=False)
middle_name = Column(String, name="middleName")
last_name = Column(String, name="lastName", nullable=False)
date_of_birth = Column(String, name="dateOfBirth")
squad_number = Column(Integer, name='squadNumber', unique=True, nullable=False)
squad_number = Column(Integer, name="squadNumber", unique=True, nullable=False)
position = Column(String, nullable=False)
abbr_position = Column(String, name='abbrPosition')
abbr_position = Column(String, name="abbrPosition")
team = Column(String)
league = Column(String)
starting11 = Column(Boolean)
8 changes: 7 additions & 1 deletion services/player_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

Handles SQLAlchemy exceptions with transaction rollback and logs errors.
"""

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import SQLAlchemyError
Expand Down Expand Up @@ -42,6 +43,7 @@ async def create_async(async_session: AsyncSession, player_model: PlayerModel):
await async_session.rollback()
return False


# Retrieve ---------------------------------------------------------------------


Expand Down Expand Up @@ -77,7 +79,9 @@ async def retrieve_by_id_async(async_session: AsyncSession, player_id: int):
return player


async def retrieve_by_squad_number_async(async_session: AsyncSession, squad_number: int):
async def retrieve_by_squad_number_async(
async_session: AsyncSession, squad_number: int
):
"""
Retrieves a Player by its Squad Number from the database.

Expand All @@ -93,6 +97,7 @@ async def retrieve_by_squad_number_async(async_session: AsyncSession, squad_numb
player = result.scalars().first()
return player


# Update -----------------------------------------------------------------------


Expand Down Expand Up @@ -127,6 +132,7 @@ async def update_async(async_session: AsyncSession, player_model: PlayerModel):
await async_session.rollback()
return False


# Delete -----------------------------------------------------------------------


Expand Down
4 changes: 2 additions & 2 deletions tests/player_stub.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def __init__(
abbr_position=None,
team=None,
league=None,
starting11=None
starting11=None,
):
self.id = id
self.first_name = first_name
Expand Down Expand Up @@ -77,5 +77,5 @@ def unknown_player():
first_name="John",
last_name="Doe",
squad_number="999",
position="Lipsum"
position="Lipsum",
)
Loading
Loading