From 1072e1a88f2ad62da2bf64653257a028c7ccc177 Mon Sep 17 00:00:00 2001 From: Jay Miller Date: Wed, 24 Jan 2024 14:57:04 -0500 Subject: [PATCH] Cruft Update (#14) --- .cruft.json | 8 +-- .devcontainer/devcontainer.json | 5 +- .github/workflows/azure-dev.yml | 2 +- .github/workflows/tests.yml | 81 ++++++++++++++++---------- .vscode/launch.json | 11 +++- README.md | 22 ++++++- pyproject.toml | 3 + requirements-dev.txt | 3 + src/fastapi_app/models.py | 2 +- src/pyproject.toml | 12 ++-- src/static/manifest.json | 30 ++++++++++ src/templates/base.html | 17 ++++-- src/templates/info_request_create.html | 4 +- src/tests/local/conftest.py | 4 +- src/tests/local/test_gunicorn.py | 15 +++++ src/tests/local/test_playwright.py | 13 +++++ 16 files changed, 176 insertions(+), 56 deletions(-) create mode 100644 src/static/manifest.json create mode 100644 src/tests/local/test_gunicorn.py diff --git a/.cruft.json b/.cruft.json index aa72572..a350527 100644 --- a/.cruft.json +++ b/.cruft.json @@ -1,6 +1,6 @@ { - "template": "https://github.com/kjaymiller/cookiecutter-relecloud/", - "commit": "62bc1afd3ca0eb0daf28a7c829761198033a08a6", + "template": "https://github.com/Azure-Samples/Azure-Python-Standardization-Template-Generator", + "commit": "621af12f962b64b1f3458b83fa1af0898d565d80", "checkout": null, "context": { "cookiecutter": { @@ -11,7 +11,7 @@ "project_host": "aca", "web_port": "8000", "__repo_name": "azure-fastapi-cosmos-postgres-aca", - "__src_folder_name": "azure_fastapi_cosmos_postgres_aca", + "__src_folder_name": "azure-fastapi-cosmos-postgres-aca", "__project_short_description": "Create a relecloud demo application with fastapi and cosmos-postgres", "_copy_without_render": [ ".github/workflows/azure-dev.yml", @@ -26,7 +26,7 @@ "lstrip_blocks": true, "trim_blocks": true }, - "_template": "https://github.com/kjaymiller/cookiecutter-relecloud/" + "_template": "https://github.com/Azure-Samples/Azure-Python-Standardization-Template-Generator" } }, "directory": null diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ecd2e4e..dc6fb7c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,7 +1,7 @@ // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/docker-existing-docker-compose { - "name": "azure_fastapi_cosmos_postgres_aca", + "name": "azure-fastapi-cosmos-postgres-aca", // Update the 'dockerComposeFile' list if you have more compose files or use different names. // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. @@ -32,7 +32,8 @@ "files.exclude": { ".coverage": true, ".pytest_cache": true, - "__pycache__": true + "__pycache__": true, + ".ruff_cache": true }, "[python]": { "editor.formatOnSave": true, diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 3013f96..f825973 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -103,5 +103,5 @@ jobs: run: | python3 -m pip install --upgrade pip python3 -m pip install -r requirements-dev.txt - python3 -m playwright install --with-deps + python3 -m playwright install chromium --with-deps python3 -m pytest --exitfirst src/tests/smoke/smoketests.py --live-server-url $URI diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a35fabb..fd6a60c 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -14,37 +14,56 @@ jobs: strategy: fail-fast: false matrix: - os: ["ubuntu-20.04"] + os: ["ubuntu-latest", "macos-latest", "macos-latest-xlarge", "windows-latest"] python_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] - services: - db: - image: postgres:14 - env: - POSTGRES_PASSWORD: postgres - ports: - - 5432:5432 - # needed because the postgres container does not provide a healthcheck - options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + exclude: + - os: macos-latest-xlarge + python_version: 3.8 + - os: macos-latest-xlarge + python_version: 3.9 + - os: macos-latest-xlarge + python_version: "3.10" steps: - - uses: actions/checkout@v3 - - name: Setup python - uses: actions/setup-python@v2 - with: + - name: Checkout + uses: actions/checkout@v3 + - name: Check for MacOS Runner + if: matrix.os == 'macos-latest-xlarge' + run: brew install postgresql@14 + - name: Setup postgres + uses: ikalnytskyi/action-setup-postgres@v4 + - name: Setup python + uses: actions/setup-python@v2 + with: - python-version: ${{ matrix.python_version }} - architecture: x64 - - name: Install dependencies - run: | - python3 -m pip install --upgrade pip - python3 -m pip install -r requirements-dev.txt - playwright install --with-deps - python3 -m pip install -e src - - name: Seed data and run Pytest tests - run: | - python3 src/fastapi_app/seed_data.py - python3 -m pytest - env: - POSTGRES_HOST: localhost - POSTGRES_USERNAME: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DATABASE: postgres + python-version: ${{ matrix.python_version }} + architecture: x64 + - name: Install dependencies + run: | + python3 -m pip install --upgrade pip + python3 -m pip install -r requirements-dev.txt + playwright install chromium --with-deps + python3 -m pip install -e src + - name: Seed data + run: | + python3 src/fastapi_app/seed_data.py + env: + POSTGRES_HOST: localhost + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DATABASE: postgres + - name: Run tests Windows + if: runner.os == 'windows' + run: python3 -m pytest --ignore=src/tests/local/test_gunicorn.py + env: + POSTGRES_HOST: localhost + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DATABASE: postgres + - name: Run tests + if: runner.os != 'windows' + run: python3 -m pytest + env: + POSTGRES_HOST: localhost + POSTGRES_USERNAME: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DATABASE: postgres \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index f1aa0c4..cd7c512 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -13,6 +13,15 @@ "fastapi_app:app", "--reload" ], + }, + { + "name": "Python: Debug Tests", + "type": "python", + "request": "launch", + "program": "${file}", + "purpose": ["debug-test"], + "console": "integratedTerminal", + "env": {"PYTEST_ADDOPTS": "--no-cov"} } ] -} \ No newline at end of file +} diff --git a/README.md b/README.md index 0909470..f4d0a32 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ +--- +page_type: sample +languages: +- azdeveloper +- python +- bicep +- html +- css +- scss +products: +- azure +- azure-container-apps +- azure-postgresql +- azure-cosmos-db +urlFragment: azure-fastapi-cosmos-postgres-aca +name: Deploy FastAPI Application with PostgreSQL on Azure Container Apps (Python) +description: This project deploys a web application for a space travel agency using FastAPI with Python, and is set up for easy deployment with the Azure Developer CLI. +--- + + # Deploy FastAPI Application with PostgreSQL via Azure Container Apps This project deploys a web application for a space travel agency using FastAPI. The application can be deployed to Azure with Azure Container Apps using the [Azure Developer CLI](https://learn.microsoft.com/azure/developer/azure-developer-cli/overview). @@ -55,7 +75,7 @@ python3 -m uvicorn fastapi_app:app --reload --port=8000 ```sh python3 -m pip install -r requirements-dev.txt - python3 -m playwright install --with-deps + python3 -m playwright install chromium --with-deps ``` 3. Run the tests: diff --git a/pyproject.toml b/pyproject.toml index 4d262c0..1e0a350 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,3 +9,6 @@ known-first-party = ["fastapi_app"] [tool.pytest.ini_options] addopts = "-ra -vv" + +[tool.coverage.report] +show_missing = true diff --git a/requirements-dev.txt b/requirements-dev.txt index c2c4369..6784c50 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,6 +8,9 @@ pip-tools pytest ephemeral-port-reserve pytest-playwright +coverage +pytest-cov +axe-playwright-python # Linters ruff diff --git a/src/fastapi_app/models.py b/src/fastapi_app/models.py index da3b655..201a705 100644 --- a/src/fastapi_app/models.py +++ b/src/fastapi_app/models.py @@ -16,7 +16,7 @@ if POSTGRES_SSL: sql_url = f"{sql_url}?sslmode={POSTGRES_SSL}" -engine = create_engine(sql_url, echo=True) +engine = create_engine(sql_url) def create_db_and_tables(): diff --git a/src/pyproject.toml b/src/pyproject.toml index aafac4d..658f84b 100644 --- a/src/pyproject.toml +++ b/src/pyproject.toml @@ -3,12 +3,12 @@ name = "fastapi_app" version = "1.0.0" description = "Create a relecloud demo application with fastapi and cosmos-postgres" dependencies = [ - "fastapi==0.100.1", - "jinja2==3.1.2", - "uvicorn[standard]==0.23.2", - "python-multipart==0.0.6", - "psycopg2-binary==2.9.6", - "sqlmodel==0.0.8", + "fastapi", + "jinja2", + "uvicorn[standard]", + "python-multipart", + "psycopg2", + "sqlmodel", ] [build-system] diff --git a/src/static/manifest.json b/src/static/manifest.json new file mode 100644 index 0000000..6abc49c --- /dev/null +++ b/src/static/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "ReleCloud Space Tourism", + "short_name": "ReleCloud", + "start_url": ".", + "display": "standalone", + "background_color": "#fff", + "description": "ReleCloud Space Tourism", + "icons": [ + { + "src": "/static/res/img/favicon.ico", + "sizes": "32x32", + "type": "image/x-icon" + }, + { + "src": "/static/res/img/favicon-120-precomposed.png", + "sizes": "120x120", + "type": "image/png" + }, + { + "src": "/static/res/img/favicon-152-precomposed.png", + "sizes": "152x152", + "type": "image/png" + }, + { + "src": "/static/res/img/favicon-192.png", + "sizes": "192x192", + "type": "image/png" + } + ] +} diff --git a/src/templates/base.html b/src/templates/base.html index 20bda0f..cfe04a2 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -4,21 +4,26 @@ {% if prod %} {% endif %} - + - + - + - + - + - + diff --git a/src/templates/info_request_create.html b/src/templates/info_request_create.html index aae22ba..770dc6a 100644 --- a/src/templates/info_request_create.html +++ b/src/templates/info_request_create.html @@ -17,13 +17,13 @@ <h1 id="page-title">Request information</h2> <p>Fill out the form below to request information about our cruises</p> <form method="post"> - <form method="post" action="/info_request/"> + <form method="post" action="/info_request"> <label for="name">Name:</label> <input type="text" id="name" name="name" required><br><br> <label for="email">Email:</label> <input type="email" id="email" name="email" required><br><br> <label for="cruise_id">Cruise:</label> - <select name="cruise_id"> + <select name="cruise_id" id="cruise_id"> {% for cruise in cruises %} <option value="{{ cruise.id }}">{{ cruise.name }}</option> {% endfor %} diff --git a/src/tests/local/conftest.py b/src/tests/local/conftest.py index 8ac6d7e..d0ca094 100644 --- a/src/tests/local/conftest.py +++ b/src/tests/local/conftest.py @@ -1,4 +1,5 @@ import multiprocessing +import sys import time import ephemeral_port_reserve @@ -10,7 +11,8 @@ from fastapi_app.app import app # Set start method to "fork" to avoid issues with pickling on OSes that default to "spawn" -multiprocessing.set_start_method("fork") +if sys.platform != "win32": + multiprocessing.set_start_method("fork") def wait_for_server_ready(url: str, timeout: float = 10.0, check_interval: float = 0.5) -> bool: diff --git a/src/tests/local/test_gunicorn.py b/src/tests/local/test_gunicorn.py new file mode 100644 index 0000000..781cd79 --- /dev/null +++ b/src/tests/local/test_gunicorn.py @@ -0,0 +1,15 @@ +import sys +from unittest import mock + +import pytest +from gunicorn.app.wsgiapp import run + + +def test_config_imports(): + argv = ["gunicorn", "--check-config", "fastapi_app.app:app", "-c", "src/gunicorn.conf.py"] + + with mock.patch.object(sys, "argv", argv): + with pytest.raises(SystemExit) as excinfo: + run() + + assert excinfo.value.args[0] == 0 diff --git a/src/tests/local/test_playwright.py b/src/tests/local/test_playwright.py index 2b20c4c..3c05c4d 100644 --- a/src/tests/local/test_playwright.py +++ b/src/tests/local/test_playwright.py @@ -1,13 +1,20 @@ import re import pytest +from axe_playwright_python.sync_playwright import Axe from playwright.sync_api import Page, expect +def check_for_violations(page: Page): + results = Axe().run(page) + assert results.violations_count == 0, results.generate_report() + + def test_home(page: Page, live_server_url: str): """Test that the home page loads""" page.goto(live_server_url) expect(page).to_have_title("ReleCloud - Expand your horizons") + check_for_violations(page) @pytest.mark.parametrize( @@ -26,6 +33,7 @@ def test_header_has_request_info(page: Page, live_server_url: str, page_title, p # Request Info request_info = header.get_by_role("link", name=page_title) expect(request_info).to_have_attribute("href", re.compile(rf".*{page_url}.*")) + check_for_violations(page) def test_request_information(page: Page, live_server_url: str): @@ -33,12 +41,14 @@ def test_request_information(page: Page, live_server_url: str): page.goto(live_server_url) page.get_by_role("link", name="Request Information").click() expect(page).to_have_title("ReleCloud - Request information") + check_for_violations(page) def test_destinations(page: Page, live_server_url: str): page.goto(live_server_url) page.get_by_role("link", name="Destinations").click() expect(page).to_have_title("ReleCloud - Destinations") + check_for_violations(page) destinations = ( @@ -80,6 +90,7 @@ def test_destination_options( page.get_by_role("link", name="Destinations").click() expect(page).to_have_title("ReleCloud - Destinations") expect(page.get_by_text(destination)).to_be_visible() + check_for_violations(page) @pytest.mark.parametrize( @@ -97,6 +108,7 @@ def test_destination_options_have_cruises(page: Page, live_server_url: str, dest for page_cruise in page_cruises: assert page_cruise.text_content() in cruises + check_for_violations(page) def test_about(page: Page, live_server_url: str): @@ -104,3 +116,4 @@ def test_about(page: Page, live_server_url: str): page.goto(live_server_url) page.get_by_role("link", name="About").click() expect(page.locator("#page-title")).to_have_text(re.compile(r".*about.*", re.IGNORECASE)) + check_for_violations(page)