From 635b7b069e97ea3bb25de7960b20616b9490ddf8 Mon Sep 17 00:00:00 2001 From: Nano Taboada Date: Sun, 23 Mar 2025 21:28:29 -0300 Subject: [PATCH 1/3] refactor(ci)!: split workflow jobs into lint, test and coverage --- .flake8 | 27 +++++- .github/workflows/python-app.yml | 150 ++++++++++++++++--------------- 2 files changed, 104 insertions(+), 73 deletions(-) diff --git a/.flake8 b/.flake8 index cb7fe65..2fa7fdf 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,28 @@ [flake8] -# The GitHub editor is 127 chars wide +# Maximum line length allowed max-line-length = 127 + +# Maximum allowed code complexity (10 is a reasonable threshold) +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) +exclude = .venv + +# Print the count of linting errors +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 cf19a12..991e3d6 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -9,82 +9,88 @@ on: pull_request: branches: [ master ] -jobs: +env: + PYTHON_VERSION: 3.9 - build: +jobs: + lint: runs-on: ubuntu-latest steps: - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Python 3.9 - uses: actions/setup-python@v5 - with: - python-version: 3.9 - cache: 'pip' - - - name: Install packages with pip - run: | - python -m pip install --upgrade pip - pip install flake8 pytest pytest-cov - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - - name: Lint with Flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - - name: Run tests with pytest - run: | - pytest -v - - - name: Generate coverage report with pytest-cov - run: | - pytest --cov=./ --cov-report=xml --cov-report=term - - - name: Upload coverage report artifact - uses: actions/upload-artifact@v4 - with: - name: coverage.xml - path: ./coverage.xml - - coverage-codecov: - needs: build + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-lint.txt + + - name: Lint with Flake8 + run: | + flake8 . + + test: + needs: lint runs-on: ubuntu-latest steps: - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download coverage report artifact - uses: actions/download-artifact@v4 - with: - name: coverage.xml - - - name: Upload coverage report to Codecov - uses: codecov/codecov-action@v5 - with: - token: ${{ secrets.CODECOV_TOKEN }} - files: coverage.xml - - coverage-codacy: - needs: build + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements-test.txt + + - name: Run tests with pytest + run: | + pytest -v + + - name: Generate coverage report + run: | + pytest --cov=./ --cov-report=xml --cov-report=term + + - name: Upload coverage report artifact + uses: actions/upload-artifact@v4 + with: + name: coverage.xml + path: ./coverage.xml + + coverage: + needs: test runs-on: ubuntu-latest + strategy: + matrix: + service: [codecov, codacy] steps: - - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Download coverage report artifact - uses: actions/download-artifact@v4 - with: - name: coverage.xml - - - name: Upload coverage report to Codacy - uses: codacy/codacy-coverage-reporter-action@v1 - with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: coverage.xml + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Download coverage report artifact + uses: actions/download-artifact@v4 + with: + name: coverage.xml + + - name: Upload coverage report to ${{ matrix.service }} + if: ${{ matrix.service == 'codecov' }} + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: coverage.xml + + - name: Upload coverage report to ${{ matrix.service }} + if: ${{ matrix.service == 'codacy' }} + uses: codacy/codacy-coverage-reporter-action@v1 + with: + project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} + coverage-reports: coverage.xml From 8db5920b4987b1e12737522079fd06539123de58 Mon Sep 17 00:00:00 2001 From: Nano Taboada Date: Sun, 23 Mar 2025 21:28:47 -0300 Subject: [PATCH 2/3] refactor!: split requirements files --- requirements-lint.txt | 1 + requirements-test.txt | 6 ++++ requirements.txt | 65 ++----------------------------------------- 3 files changed, 10 insertions(+), 62 deletions(-) create mode 100644 requirements-lint.txt create mode 100644 requirements-test.txt diff --git a/requirements-lint.txt b/requirements-lint.txt new file mode 100644 index 0000000..5c2057b --- /dev/null +++ b/requirements-lint.txt @@ -0,0 +1 @@ +flake8==7.1.2 diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..bd15249 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,6 @@ +-r requirements.txt + +pytest==8.3.5 +pytest-cov==6.0.0 +pytest-sugar==1.0.0 +gevent==24.11.1 diff --git a/requirements.txt b/requirements.txt index e674d8a..af56cb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,64 +1,5 @@ -aiohttp==3.11.14 -aiosignal==1.3.2 -aiosqlite==0.21.0 -annotated-types==0.7.0 -anyio==4.8.0 -attrs==25.3.0 -certifi==2025.1.31 -click==8.1.8 -coverage==7.7.0 -dnspython==2.7.0 -email_validator==2.2.0 -fastapi==0.115.11 +# https://fastapi.tiangolo.com/#standard-dependencies +fastapi[standard]==0.115.12 fastapi-cache2==0.2.2 -fastapi-cli==0.0.7 -flake8==7.1.2 -frozenlist==1.5.0 -gevent==24.11.1 -greenlet==3.1.1 -h11==0.14.0 -httpcore==1.0.7 -httptools==0.6.4 -httpx==0.28.1 -idna==3.10 -iniconfig==2.0.0 -Jinja2==3.1.5 -markdown-it-py==3.0.0 -MarkupSafe==3.0.2 -mccabe==0.7.0 -mdurl==0.1.2 -multidict==6.1.0 -packaging==24.2 -pendulum==3.0.0 -pluggy==1.5.0 -pycodestyle==2.12.1 -pydantic==2.10.6 -pydantic_core -pyflakes==3.2.0 -Pygments==2.19.1 -pytest==8.3.5 -pytest-cov==6.0.0 -pytest-sugar==1.0.0 -python-dateutil==2.9.0.post0 -python-dotenv==1.0.1 -python-multipart==0.0.20 -PyYAML==6.0.2 -rich==13.9.4 -setuptools==76.0.0 -shellingham==1.5.4 -six==1.17.0 -sniffio==1.3.1 SQLAlchemy==2.0.39 -starlette -termcolor==2.5.0 -time-machine==2.16.0 -typer==0.15.2 -typing_extensions==4.12.2 -tzdata==2025.1 -uvicorn==0.34.0 -uvloop==0.21.0 -watchfiles==1.0.4 -websockets==15.0.1 -yarl==1.18.3 -zope.event==5.0 -zope.interface==7.2 +aiosqlite==0.21.0 From aea9c50eac5c54813d4f54819b7b5aca7c36e97e Mon Sep 17 00:00:00 2001 From: Nano Taboada Date: Sun, 23 Mar 2025 21:32:12 -0300 Subject: [PATCH 3/3] chore(tests): include AAA (Arrange, Act, Assert) pattern --- tests/test_main.py | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index b768a2e..cb46689 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -16,7 +16,9 @@ def test_given_get_when_request_path_has_no_id_then_response_status_code_should_ when request path has no ID then response Status Code should be 200 OK """ + # Act response = client.get(PATH) + # Assert assert response.status_code == 200 @@ -26,7 +28,9 @@ def test_given_get_when_request_path_has_no_id_then_response_body_should_be_coll when request path has no ID then response Status Code should be collection of players """ + # Act response = client.get(PATH) + # Assert players = response.json() player_id = 0 for player in players: @@ -42,8 +46,11 @@ def test_given_get_when_request_path_is_nonexistent_id_then_response_status_code when request path is nonexistent ID then response Status Code should be 404 (Not Found) """ + # Arrange player_id = nonexistent_player().id + # Act response = client.get(PATH + str(player_id)) + # Assert assert response.status_code == 404 @@ -53,8 +60,11 @@ def test_given_get_when_request_path_is_existing_id_then_response_status_code_sh when request path is existing ID then response Status Code should be 200 (OK) """ + # Arrange player_id = existing_player().id + # Act response = client.get(PATH + str(player_id)) + # Assert assert response.status_code == 200 @@ -64,8 +74,11 @@ def test_given_get_when_request_path_is_existing_id_then_response_body_should_be when request path is existing ID then response body should be matching Player """ + # Arrange player_id = existing_player().id + # Act response = client.get(PATH + str(player_id)) + # Assert player = response.json() assert player["id"] == player_id @@ -78,8 +91,11 @@ def test_given_get_when_request_path_is_nonexistent_squad_number_then_response_s when request path is nonexistent Squad Number then response Status Code should be 404 (Not Found) """ + # Arrange squad_number = nonexistent_player().squad_number + # Act response = client.get(PATH + "squadnumber" + "/" + str(squad_number)) + # Assert assert response.status_code == 404 @@ -89,8 +105,11 @@ def test_given_get_when_request_path_is_existing_squad_number_then_response_stat when request path is existing Squad Number then response Status Code should be 200 (OK) """ + # Arrange squad_number = existing_player().squad_number + # Act response = client.get(PATH + "squadnumber" + "/" + str(squad_number)) + # Assert assert response.status_code == 200 @@ -100,8 +119,11 @@ def test_given_get_when_request_path_is_existing_squad_number_then_response_body when request path is existing Squad Number then response body should be matching Player """ + # Arrange squad_number = existing_player().squad_number + # Act response = client.get(PATH + "squadnumber" + "/" + str(squad_number)) + # Assert player = response.json() assert player["squadNumber"] == squad_number @@ -114,8 +136,11 @@ def test_given_post_when_request_body_is_empty_then_response_status_code_should_ when request body is empty then response Status Code should be 422 (Unprocessable Entity) """ + # Arrange body = {} + # Act response = client.post(PATH, data=body) + # Assert assert response.status_code == 422 @@ -125,9 +150,12 @@ def test_given_post_when_request_body_is_existing_player_then_response_status_co when request body is existing Player then response Status Code should be 409 (Conflict) """ + # Arrange player = existing_player() body = json.dumps(player.__dict__) + # Act response = client.post(PATH, data=body) + # Assert assert response.status_code == 409 @@ -137,9 +165,12 @@ def test_given_post_when_request_body_is_nonexistent_player_then_response_status when request body is nonexistent Player then response Status Code should be 201 (Created) """ + # Arrange player = nonexistent_player() body = json.dumps(player.__dict__) + # Act response = client.post(PATH, data=body) + # Assert assert response.status_code == 201 # PUT /players/{player_id} ----------------------------------------------------- @@ -151,9 +182,12 @@ def test_given_put_when_request_body_is_empty_then_response_status_code_should_b when request body is empty then response Status Code should be 422 (Unprocessable Entity) """ + # Arrange player_id = existing_player().id body = {} + # Act response = client.put(PATH + str(player_id), data=body) + # Assert assert response.status_code == 422 @@ -163,10 +197,13 @@ def test_given_put_when_request_path_is_unknown_id_then_response_status_code_sho when request path is unknown ID then response Status Code should be 404 (Not Found) """ + # Arrange player_id = unknown_player().id player = unknown_player() body = json.dumps(player.__dict__) + # Act response = client.put(PATH + str(player_id), data=body) + # Assert assert response.status_code == 404 @@ -176,12 +213,15 @@ def test_given_put_when_request_path_is_existing_id_then_response_status_code_sh when request path is existing ID then response Status Code should be 204 (No Content) """ + # Arrange player_id = existing_player().id player = existing_player() player.first_name = "Emiliano" player.middle_name = "" body = json.dumps(player.__dict__) + # Act response = client.put(PATH + str(player_id), data=body) + # Assert assert response.status_code == 204 # DELETE /players/{player_id} -------------------------------------------------- @@ -193,8 +233,11 @@ def test_given_delete_when_request_path_is_unknown_id_then_response_status_code_ when request path is unknown ID then response Status Code should be 404 (Not Found) """ + # Arrange player_id = unknown_player().id + # Act response = client.delete(PATH + str(player_id)) + # Assert assert response.status_code == 404 @@ -204,6 +247,9 @@ def test_given_delete_when_request_path_is_existing_id_then_response_status_code when request path is existing ID then response Status Code should be 204 (No Content) """ + # Arrange player_id = 12 # nonexistent_player() previously created + # Act response = client.delete(PATH + str(player_id)) + # Assert assert response.status_code == 204