diff --git a/.flake8 b/.flake8
index 2fa7fdf..dfaadb5 100644
--- a/.flake8
+++ b/.flake8
@@ -1,28 +1,11 @@
[flake8]
-# Maximum line length allowed
-max-line-length = 127
-
-# Maximum allowed code complexity (10 is a reasonable threshold)
+max-line-length = 88
max-complexity = 10
-
-# Only check for specific error codes:
-# - E9 -> Syntax errors
-# - F63 -> Issues related to improper usage of `+=`, `-=`, etc.
-# - F7 -> Issues related to improper `break`, `continue`, etc.
-# - F82 -> Undefined names
-select = E9,F63,F7,F82
-
-# Exclude `.venv` from linting (prevents checking dependencies)
+select = E,F,W
+extend-ignore = E203, W503
exclude = .venv
-
-# Print the count of linting errors
+per-file-ignores = tests/test_main.py: E501
count = True
-
-# Show the exact source of the error
show-source = True
-
-# Display statistics of errors at the end of the report
statistics = True
-
-# Enable verbose mode for more detailed output
verbose = True
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
index 4020e8c..9822764 100644
--- a/.github/workflows/python-app.yml
+++ b/.github/workflows/python-app.yml
@@ -31,7 +31,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- - name: Install dependencies
+ - name: Install lint dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-lint.txt
@@ -40,6 +40,10 @@ jobs:
run: |
flake8 .
+ - name: Check code formatting with Black
+ run: |
+ black --check .
+
test:
needs: lint
runs-on: ubuntu-latest
@@ -53,7 +57,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- - name: Install dependencies
+ - name: Install test dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements-test.txt
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index b657926..b9d9500 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -1,7 +1,8 @@
{
"recommendations": [
"ms-python.python",
- "ms-python.vscode-pylance",
+ "ms-python.flake8",
+ "ms-python.black-formatter",
"github.vscode-pull-request-github",
"github.vscode-github-actions",
"ms-azuretools.vscode-containers",
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 401becc..6110b19 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,16 +1,53 @@
{
- "python.testing.unittestEnabled": false,
- "python.testing.pytestEnabled": true,
- "python.testing.pytestArgs": ["tests"],
"files.exclude": {
"**/__pycache__": true,
"**/.git": true,
"**/.DS_Store": true
},
+ "editor.wordWrapColumn": 88,
+ "editor.rulers": [88],
"[python]": {
- "editor.defaultFormatter": "ms-python.autopep8",
+ "editor.defaultFormatter": "ms-python.black-formatter",
"editor.formatOnSave": true
},
+ "python.testing.unittestEnabled": false,
+ "python.testing.pytestEnabled": true,
+ "python.testing.pytestArgs": ["tests"],
+ "flake8.enabled": true,
+ "flake8.importStrategy": "fromEnvironment",
+ "flake8.path": ["${interpreter}", "-m", "flake8"],
+ // Point flake8 to use your existing config file automatically
+ "flake8.args": [
+ "--max-line-length=88",
+ "--max-complexity=10",
+ "--select=E,F,W",
+ "--extend-ignore=E203,W503",
+ "--exclude=.venv",
+ "--per-file-ignores=tests/test_main.py:E501"
+ ],
+ // Exclude files/folders you don’t want to lint (matching Black’s exclude)
+ "flake8.ignorePatterns": [
+ "**/.git/**",
+ "**/.github/**",
+ "**/.pytest_cache/**",
+ "**/.venv/**",
+ "**/.vscode/**",
+ "**/assets/**",
+ "**/htmlcov/**",
+ "**/postman_collections/**",
+ "**/scripts/**",
+ "**/storage/**",
+ "**/__pycache__/**",
+ "**/tests/test_main.py"
+ ],
+ "flake8.severity": {
+ "convention": "Information",
+ "error": "Error",
+ "fatal": "Error",
+ "refactor": "Hint",
+ "warning": "Warning",
+ "info": "Information"
+ },
"sonarlint.connectedMode.project": {
"connectionId": "nanotaboada",
"projectKey": "nanotaboada_python-samples-fastapi-restful"
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e46e622..e63ea16 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -10,43 +10,58 @@ We value **incremental, detail‑first contributions** over big rewrites or abst
## 2. Code & Commit Conventions
- **Conventional Commits**
- Follow :
- - `feat: …` for new features
- - `fix: …` for bug fixes
- - `chore: …` for maintenance
+ Follow :
+ - `feat: ` for new features
+ - `fix: ` for bug fixes
+ - `chore: ` for maintenance or tooling
- **Logical Commits**
- Group changes by purpose. It’s okay to have multiple commits in a PR, but if they’re mere checkpoints, squash them into a single logical commit.
-
-- **Lint & Tests**
- Run existing linters/formatters and ensure all tests pass.
+ Group changes by purpose. Multiple commits are fine, but avoid noise. Squash when appropriate.
+
+- **Python Formatting & Style**
+ - Use **[Black](https://black.readthedocs.io/)** for consistent code formatting.
+ - Black is opinionated: don't argue with it, just run it.
+ - Line length is set to **88**, matching the default.
+ - Use **[flake8](https://flake8.pycqa.org/en/latest/)** for static checks.
+ - Line length also set to 88.
+ - Some flake8 warnings are disabled (e.g. `E203`, `W503`) to avoid conflicts with Black.
+ - Run `black .` and `flake8` before submitting.
+ - Use Python **3.13.x** for local testing and formatting.
+
+- **Testing**
+ - Run `pytest` before pushing.
+ - Ensure coverage isn’t regressing.
## 3. Pull Request Workflow
- **One logical change per PR.**
-- **Rebase or squash** before opening to keep history concise.
+- **Rebase or squash** before opening to keep history clean.
- **Title & Description**
- - Title uses Conventional Commits style.
- - Description explains _what_ and _why_—keep context minimal.
+ - Use Conventional Commit format.
+ - Explain _what_ and _why_ concisely in the PR body.
## 4. Issue Reporting
-- Search existing issues first.
-- Provide a minimal reproducible example and clear steps.
+- Search open issues before creating a new one.
+- Include clear steps to reproduce and environment details.
+- Prefer **focused** issues—don’t bundle multiple topics.
## 5. Automation & Checks
-We enforce quality via CI on every push and PR:
+All PRs and pushes go through CI:
-- **Commitlint** for commit‑message style
-- **Linters/Formatters**
-- **Unit tests**
+- **Commitlint** for commit style
+- **Black** for formatting
+- **flake8** for static checks
+- **pytest** with coverage
-Failures must be fixed before review.
+PRs must pass all checks to be reviewed.
## 6. 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.
+- See `CODE_OF_CONDUCT.md` for guidelines and reporting.
+- For questions or planning, open an issue and use the `discussion` label, or mention a maintainer.
+
+---
-Thanks again for helping keep this project small, simple, and impactful!
+Thanks again for helping keep this project small, sharp, and focused.
diff --git a/README.md b/README.md
index 7bd31b2..cdacc6d 100644
--- a/README.md
+++ b/README.md
@@ -8,6 +8,7 @@
[](https://codecov.io/gh/nanotaboada/python-samples-fastapi-restful)
[](https://www.codefactor.io/repository/github/nanotaboada/python-samples-fastapi-restful)
[](https://codebeat.co/projects/github-com-nanotaboada-python-samples-fastapi-restful-master)
+[](https://github.com/psf/black)
## About
diff --git a/databases/player_database.py b/databases/player_database.py
index a18607d..9a5fd5d 100644
--- a/databases/player_database.py
+++ b/databases/player_database.py
@@ -8,6 +8,7 @@
The `STORAGE_PATH` environment variable controls the SQLite file location.
"""
+
import logging
import os
from typing import AsyncGenerator
@@ -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()
diff --git a/main.py b/main.py
index 14e912d..a1167f0 100644
--- a/main.py
+++ b/main.py
@@ -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
@@ -17,6 +18,7 @@
UVICORN_LOGGER = "uvicorn.error"
logger = logging.getLogger(UVICORN_LOGGER)
+
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
"""
@@ -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)
diff --git a/models/player_model.py b/models/player_model.py
index 7b3f419..0ecaf5a 100644
--- a/models/player_model.py
+++ b/models/player_model.py
@@ -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
@@ -22,8 +23,10 @@ class MainModel(BaseModel):
model_config (ConfigDict): Configuration for Pydantic models, including:
alias_generator (function): A function to generate field aliases.
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.
+ 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)
@@ -39,11 +42,14 @@ class PlayerModel(MainModel):
date_of_birth (Optional[str]): The date of birth of the Player, if provided.
squad_number (int): The unique squad number assigned to the Player.
position (str): The playing position of the Player.
- abbr_position (Optional[str]): The abbreviated form of the Player's position, if any.
+ abbr_position (Optional[str]): The abbreviated form of the Player's position,
+ if any.
team (Optional[str]): The team to which the Player belongs, if any.
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.
+ starting11 (Optional[bool]): Indicates if the Player is in the starting 11,
+ if provided.
"""
+
id: int
first_name: str
middle_name: Optional[str]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..484ce82
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,20 @@
+[tool.black]
+line-length = 88
+target-version = ['py313']
+include = '\.pyi?$'
+exclude = '''
+/(
+ \.git
+ | \.github
+ | \.pytest_cache
+ | \.venv
+ | \.vscode
+ | assets
+ | htmlcov
+ | postman_collections
+ | scripts
+ | storage
+ | __pycache__
+ | tests/test_main\.py
+)/
+'''
diff --git a/requirements-lint.txt b/requirements-lint.txt
index 3062a4f..6c1f95d 100644
--- a/requirements-lint.txt
+++ b/requirements-lint.txt
@@ -1 +1,2 @@
flake8==7.2.0
+black==25.1.0
diff --git a/routes/health_route.py b/routes/health_route.py
index 838f454..0db6333 100644
--- a/routes/health_route.py
+++ b/routes/health_route.py
@@ -4,13 +4,16 @@
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():
"""
- Simple health check endpoint. Returns a JSON response with a single key "status" and value "ok".
+ Simple health check endpoint.
+ Returns a JSON response with a single key "status" and value "ok".
"""
return {"status": "ok"}
diff --git a/routes/player_route.py b/routes/player_route.py
index 39e55bf..ddeecc8 100644
--- a/routes/player_route.py
+++ b/routes/player_route.py
@@ -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
@@ -29,7 +30,7 @@
simple_memory_cache = SimpleMemoryCache()
CACHE_KEY = "players"
-CACHE_TTL = 600 # 10 minutes
+CACHE_TTL = 600 # 10 minutes
# POST -------------------------------------------------------------------------
@@ -38,17 +39,18 @@
"/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.
Args:
- player_model (PlayerModel): The Pydantic model representing the Player to create.
+ player_model (PlayerModel): The Pydantic model representing the Player to
+ create.
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
Raises:
@@ -60,6 +62,7 @@ async def post_async(
await player_service.create_async(async_session, player_model)
await simple_memory_cache.clear(CACHE_KEY)
+
# GET --------------------------------------------------------------------------
@@ -68,11 +71,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.
@@ -97,11 +99,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.
@@ -114,7 +116,8 @@ async def get_by_id_async(
PlayerModel: The Pydantic model representing the matching Player.
Raises:
- HTTPException: Not found error if the Player with the specified ID does not exist.
+ HTTPException: Not found error if the Player with the specified ID does not
+ exist.
"""
player = await player_service.retrieve_by_id_async(async_session, player_id)
if not player:
@@ -127,11 +130,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.
@@ -144,13 +147,17 @@ async def get_by_squad_number_async(
PlayerModel: The Pydantic model representing the matching Player.
Raises:
- HTTPException: HTTP 404 Not Found error if the Player with the specified Squad Number does not exist.
+ 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 --------------------------------------------------------------------------
@@ -158,23 +165,25 @@ async def get_by_squad_number_async(
"/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.
Args:
player_id (int): The ID of the Player to update.
- player_model (PlayerModel): The Pydantic model representing the Player to update.
+ player_model (PlayerModel): The Pydantic model representing the Player to
+ update.
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
Raises:
- HTTPException: HTTP 404 Not Found error if the Player with the specified ID does not exist.
+ HTTPException: HTTP 404 Not Found error if the Player with the specified ID
+ does not exist.
"""
player = await player_service.retrieve_by_id_async(async_session, player_id)
if not player:
@@ -182,6 +191,7 @@ async def put_async(
await player_service.update_async(async_session, player_model)
await simple_memory_cache.clear(CACHE_KEY)
+
# DELETE -----------------------------------------------------------------------
@@ -189,11 +199,11 @@ async def put_async(
"/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.
@@ -203,7 +213,8 @@ async def delete_async(
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
Raises:
- HTTPException: HTTP 404 Not Found error if the Player with the specified ID does not exist.
+ HTTPException: HTTP 404 Not Found error if the Player with the specified ID
+ does not exist.
"""
player = await player_service.retrieve_by_id_async(async_session, player_id)
if not player:
diff --git a/schemas/player_schema.py b/schemas/player_schema.py
index f769809..a3524a3 100644
--- a/schemas/player_schema.py
+++ b/schemas/player_schema.py
@@ -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
@@ -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)
diff --git a/services/player_service.py b/services/player_service.py
index 5464e03..5c9e894 100644
--- a/services/player_service.py
+++ b/services/player_service.py
@@ -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
@@ -26,7 +27,8 @@ async def create_async(async_session: AsyncSession, player_model: PlayerModel):
Args:
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
- player_model (PlayerModel): The Pydantic model representing the Player to create.
+ player_model (PlayerModel): The Pydantic model representing the Player to
+ create.
Returns:
True if the Player was created successfully, False otherwise.
@@ -42,6 +44,7 @@ async def create_async(async_session: AsyncSession, player_model: PlayerModel):
await async_session.rollback()
return False
+
# Retrieve ---------------------------------------------------------------------
@@ -77,7 +80,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.
@@ -93,6 +98,7 @@ async def retrieve_by_squad_number_async(async_session: AsyncSession, squad_numb
player = result.scalars().first()
return player
+
# Update -----------------------------------------------------------------------
@@ -102,7 +108,8 @@ async def update_async(async_session: AsyncSession, player_model: PlayerModel):
Args:
async_session (AsyncSession): The async version of a SQLAlchemy ORM session.
- player_model (PlayerModel): The Pydantic model representing the Player to update.
+ player_model (PlayerModel): The Pydantic model representing the Player to
+ update.
Returns:
True if the Player was updated successfully, False otherwise.
@@ -127,6 +134,7 @@ async def update_async(async_session: AsyncSession, player_model: PlayerModel):
await async_session.rollback()
return False
+
# Delete -----------------------------------------------------------------------
diff --git a/tests/player_stub.py b/tests/player_stub.py
index 0c12bc0..34bd629 100644
--- a/tests/player_stub.py
+++ b/tests/player_stub.py
@@ -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
@@ -77,5 +77,5 @@ def unknown_player():
first_name="John",
last_name="Doe",
squad_number="999",
- position="Lipsum"
+ position="Lipsum",
)
diff --git a/tests/test_main.py b/tests/test_main.py
index 9528352..c32946b 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -15,6 +15,7 @@
- Handling of existing, nonexistent, and malformed requests
- Conflict and edge case behaviors
"""
+
import json
from tests.player_stub import existing_player, nonexistent_player, unknown_player
@@ -22,11 +23,14 @@
# GET /health/ -----------------------------------------------------------------
-def test_given_get_when_request_path_is_health_then_response_status_code_should_be_200_ok(client):
+
+def test_given_get_when_request_path_is_health_then_response_status_code_is_200(
+ client,
+):
"""
- Given GET /health/
- when request
- then response Status Code should be 200 OK
+ Given GET /health/
+ when request
+ then response Status Code is 200 (OK)
"""
# Act
response = client.get("/health/")
@@ -34,14 +38,17 @@ def test_given_get_when_request_path_is_health_then_response_status_code_should_
assert response.status_code == 200
assert response.json() == {"status": "ok"}
+
# GET /players/ ----------------------------------------------------------------
-def test_given_get_when_request_is_initial_then_response_header_x_cache_should_be_miss(client):
+def test_given_get_when_request_is_initial_then_response_header_x_cache_is_miss(
+ client,
+):
"""
- Given GET /players/
- when request is initial
- then response Header X-Cache value should be MISS
+ Given GET /players/
+ when request is initial
+ then response Header X-Cache value is MISS
"""
# Act
response = client.get(PATH)
@@ -50,25 +57,31 @@ def test_given_get_when_request_is_initial_then_response_header_x_cache_should_b
assert "X-Cache" in response.headers
assert response.headers.get("X-Cache") == "MISS"
-def test_given_get_when_request_is_subsequent_then_response_header_x_cache_should_be_hit(client):
+
+def test_given_get_when_request_is_subsequent_then_response_header_x_cache_is_hit(
+ client,
+):
"""
- Given GET /players/
- when request is subsequent
- then response Header X-Cache should be HIT
+ Given GET /players/
+ when request is subsequent
+ then response Header X-Cache is HIT
"""
# Act
- client.get(PATH) # initial
- response = client.get(PATH) # subsequent (cached)
+ client.get(PATH) # initial
+ response = client.get(PATH) # subsequent (cached)
# Assert
assert "X-Cache" in response.headers
assert response.headers.get("X-Cache") == "HIT"
-def test_given_get_when_request_path_has_no_id_then_response_status_code_should_be_200_ok(client):
+
+def test_given_get_when_request_path_has_no_id_then_response_status_code_is_200(
+ client,
+):
"""
- Given GET /players/
- when request path has no ID
- then response Status Code should be 200 OK
+ Given GET /players/
+ when request path has no ID
+ then response Status Code is 200 (OK)
"""
# Act
response = client.get(PATH)
@@ -76,11 +89,13 @@ def test_given_get_when_request_path_has_no_id_then_response_status_code_should_
assert response.status_code == 200
-def test_given_get_when_request_path_has_no_id_then_response_body_should_be_collection_of_players(client):
+def test_given_get_when_request_path_has_no_id_then_response_body_is_list_of_players(
+ client,
+):
"""
- Given GET /players/
- when request path has no ID
- then response Body should be collection of players
+ Given GET /players/
+ when request path has no ID
+ then response Body is list of players
"""
# Act
response = client.get(PATH)
@@ -91,14 +106,17 @@ def test_given_get_when_request_path_has_no_id_then_response_body_should_be_coll
player_id += 1
assert player["id"] == player_id
+
# GET /players/{player_id} -----------------------------------------------------
-def test_given_get_when_request_path_is_nonexistent_id_then_response_status_code_should_be_404_not_found(client):
+def test_given_get_when_request_path_is_nonexistent_id_then_response_status_code_is_404(
+ client,
+):
"""
- Given GET /players/{player_id}
- when request path is nonexistent ID
- then response Status Code should be 404 (Not Found)
+ Given GET /players/{player_id}
+ when request path is nonexistent ID
+ then response Status Code is 404 (Not Found)
"""
# Arrange
player_id = nonexistent_player().id
@@ -108,11 +126,13 @@ def test_given_get_when_request_path_is_nonexistent_id_then_response_status_code
assert response.status_code == 404
-def test_given_get_when_request_path_is_existing_id_then_response_status_code_should_be_200_ok(client):
+def test_given_get_when_request_path_is_existing_id_then_response_status_code_is_200(
+ client,
+):
"""
- Given GET /players/{player_id}
- when request path is existing ID
- then response Status Code should be 200 (OK)
+ Given GET /players/{player_id}
+ when request path is existing ID
+ then response Status Code is 200 (OK)
"""
# Arrange
player_id = existing_player().id
@@ -122,11 +142,13 @@ def test_given_get_when_request_path_is_existing_id_then_response_status_code_sh
assert response.status_code == 200
-def test_given_get_when_request_path_is_existing_id_then_response_body_should_be_matching_player(client):
+def test_given_get_when_request_path_is_existing_id_then_response_is_matching_player(
+ client,
+):
"""
- Given GET /players/{player_id}
- when request path is existing ID
- then response body should be matching Player
+ Given GET /players/{player_id}
+ when request path is existing ID
+ then response is matching Player
"""
# Arrange
player_id = existing_player().id
@@ -136,14 +158,17 @@ def test_given_get_when_request_path_is_existing_id_then_response_body_should_be
player = response.json()
assert player["id"] == player_id
+
# GET /players/squadnumber/{squad_number} --------------------------------------
-def test_given_get_when_request_path_is_nonexistent_squad_number_then_response_status_code_should_be_404_not_found(client):
+def test_given_get_when_request_path_is_nonexistent_squad_number_then_response_status_code_is_404(
+ client,
+):
"""
- Given GET /players/squadnumber/{squad_number}
- when request path is nonexistent Squad Number
- then response Status Code should be 404 (Not Found)
+ Given GET /players/squadnumber/{squad_number}
+ when request path is nonexistent Squad Number
+ then response Status Code is 404 (Not Found)
"""
# Arrange
squad_number = nonexistent_player().squad_number
@@ -153,11 +178,13 @@ def test_given_get_when_request_path_is_nonexistent_squad_number_then_response_s
assert response.status_code == 404
-def test_given_get_when_request_path_is_existing_squad_number_then_response_status_code_should_be_200_ok(client):
+def test_given_get_when_request_path_is_existing_squad_number_then_response_status_code_is_200(
+ client,
+):
"""
- Given GET /players/squadnumber/{squad_number}
- when request path is existing Squad Number
- then response Status Code should be 200 (OK)
+ Given GET /players/squadnumber/{squad_number}
+ when request path is existing Squad Number
+ then response Status Code is 200 (OK)
"""
# Arrange
squad_number = existing_player().squad_number
@@ -167,11 +194,13 @@ def test_given_get_when_request_path_is_existing_squad_number_then_response_stat
assert response.status_code == 200
-def test_given_get_when_request_path_is_existing_squad_number_then_response_body_should_be_matching_player(client):
+def test_given_get_when_request_path_is_existing_squad_number_then_response_is_matching_player(
+ client,
+):
"""
- Given GET /players/squadnumber/{squad_number}
- when request path is existing Squad Number
- then response body should be matching Player
+ Given GET /players/squadnumber/{squad_number}
+ when request path is existing Squad Number
+ then response is matching Player
"""
# Arrange
squad_number = existing_player().squad_number
@@ -181,14 +210,17 @@ def test_given_get_when_request_path_is_existing_squad_number_then_response_body
player = response.json()
assert player["squadNumber"] == squad_number
+
# POST /players/ ---------------------------------------------------------------
-def test_given_post_when_request_body_is_empty_then_response_status_code_should_be_422_unprocessable_entity(client):
+def test_given_post_when_request_body_is_empty_then_response_status_code_is_422(
+ client,
+):
"""
- Given POST /players/
- when request body is empty
- then response Status Code should be 422 (Unprocessable Entity)
+ Given POST /players/
+ when request body is empty
+ then response Status Code is 422 (Unprocessable Entity)
"""
# Arrange
body = {}
@@ -198,11 +230,13 @@ def test_given_post_when_request_body_is_empty_then_response_status_code_should_
assert response.status_code == 422
-def test_given_post_when_request_body_is_existing_player_then_response_status_code_should_be_409_conflict(client):
+def test_given_post_when_request_body_is_existing_player_then_response_status_code_is_409(
+ client,
+):
"""
- Given POST /players/
- when request body is existing Player
- then response Status Code should be 409 (Conflict)
+ Given POST /players/
+ when request body is existing Player
+ then response Status Code is 409 (Conflict)
"""
# Arrange
player = existing_player()
@@ -213,11 +247,13 @@ def test_given_post_when_request_body_is_existing_player_then_response_status_co
assert response.status_code == 409
-def test_given_post_when_request_body_is_nonexistent_player_then_response_status_code_should_be_201_created(client):
+def test_given_post_when_request_body_is_nonexistent_player_then_response_status_code_is_201(
+ client,
+):
"""
- Given POST /players/
- when request body is nonexistent Player
- then response Status Code should be 201 (Created)
+ Given POST /players/
+ when request body is nonexistent Player
+ then response Status Code is 201 (Created)
"""
# Arrange
player = nonexistent_player()
@@ -227,14 +263,17 @@ def test_given_post_when_request_body_is_nonexistent_player_then_response_status
# Assert
assert response.status_code == 201
+
# PUT /players/{player_id} -----------------------------------------------------
-def test_given_put_when_request_body_is_empty_then_response_status_code_should_be_422_unprocessable_entity(client):
+def test_given_put_when_request_body_is_empty_then_response_status_code_is_422(
+ client,
+):
"""
- Given PUT /players/{player_id}
- when request body is empty
- then response Status Code should be 422 (Unprocessable Entity)
+ Given PUT /players/{player_id}
+ when request body is empty
+ then response Status Code is 422 (Unprocessable Entity)
"""
# Arrange
player_id = existing_player().id
@@ -245,11 +284,13 @@ def test_given_put_when_request_body_is_empty_then_response_status_code_should_b
assert response.status_code == 422
-def test_given_put_when_request_path_is_unknown_id_then_response_status_code_should_be_404_not_found(client):
+def test_given_put_when_request_path_is_unknown_id_then_response_status_code_is_404(
+ client,
+):
"""
- Given PUT /players/{player_id}
- when request path is unknown ID
- then response Status Code should be 404 (Not Found)
+ Given PUT /players/{player_id}
+ when request path is unknown ID
+ then response Status Code is 404 (Not Found)
"""
# Arrange
player_id = unknown_player().id
@@ -261,11 +302,13 @@ def test_given_put_when_request_path_is_unknown_id_then_response_status_code_sho
assert response.status_code == 404
-def test_given_put_when_request_path_is_existing_id_then_response_status_code_should_be_204_no_content(client):
+def test_given_put_when_request_path_is_existing_id_then_response_status_code_is_204(
+ client,
+):
"""
- Given PUT /players/{player_id}
- when request path is existing ID
- then response Status Code should be 204 (No Content)
+ Given PUT /players/{player_id}
+ when request path is existing ID
+ then response Status Code is 204 (No Content)
"""
# Arrange
player_id = existing_player().id
@@ -278,14 +321,17 @@ def test_given_put_when_request_path_is_existing_id_then_response_status_code_sh
# Assert
assert response.status_code == 204
+
# DELETE /players/{player_id} --------------------------------------------------
-def test_given_delete_when_request_path_is_unknown_id_then_response_status_code_should_be_404_not_found(client):
+def test_given_delete_when_request_path_is_unknown_id_then_response_status_code_is_404(
+ client,
+):
"""
- Given DELETE /players/{player_id}
- when request path is unknown ID
- then response Status Code should be 404 (Not Found)
+ Given DELETE /players/{player_id}
+ when request path is unknown ID
+ then response Status Code is 404 (Not Found)
"""
# Arrange
player_id = unknown_player().id
@@ -295,11 +341,13 @@ def test_given_delete_when_request_path_is_unknown_id_then_response_status_code_
assert response.status_code == 404
-def test_given_delete_when_request_path_is_existing_id_then_response_status_code_should_be__204_no_content(client):
+def test_given_delete_when_request_path_is_existing_id_then_response_status_code_is_204(
+ client,
+):
"""
- Given DELETE /players/{player_id}
- when request path is existing ID
- then response Status Code should be 204 (No Content)
+ Given DELETE /players/{player_id}
+ when request path is existing ID
+ then response Status Code is 204 (No Content)
"""
# Arrange
player_id = 12 # nonexistent_player() previously created