diff --git a/.github/workflows/api-docker-build-and-push.yml b/.github/workflows/api-docker-build-and-push.yml new file mode 100644 index 0000000..bfaeb66 --- /dev/null +++ b/.github/workflows/api-docker-build-and-push.yml @@ -0,0 +1,97 @@ +name: "Docker build and push API service to ghcr.io" + +on: + schedule: # Scheduled workflows only run on the default branch + - cron: '0 3 * * *' # Nightly at 3 AM UTC + push: + branches: + - main + pull_request: + branches: + - main + workflow_dispatch: + +env: + SERVICE: api + REGISTRY: ghcr.io + REPO: ${{ github.repository }} + REPO_OWNER: ${{ github.repository_owner }} + EVENT: ${{ github.event_name }} + +jobs: + build-and-push: + name: "Docker build and push API service to ghcr.io" + runs-on: ubuntu-latest + + permissions: + contents: read + packages: write # Needed to push to GHCR + + steps: + - name: Set ENV vars + run: | + # remove owner from repo URI, e.g. "owner/repo_name", to get repo name + echo "REPO_NAME=${REPO#$REPO_OWNER/}" >> "$GITHUB_ENV" + + # determine tag based on type of GH event + if [[ "$EVENT" == "schedule" ]]; then + echo "TAG=nightly" >> "$GITHUB_ENV" + elif [[ "$EVENT" == "push" ]]; then + echo "TAG=latest" >> "$GITHUB_ENV" + elif [[ "$EVENT" == "pull_request" ]]; then + echo "TAG=pr-${{ github.event.pull_request.number }}" >> "$GITHUB_ENV" + else + echo "TAG=unknown" >> "$GITHUB_ENV" + fi + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build and start container (as backing service) + run: | + docker compose up --build --detach $SERVICE + + - name: Wait for container to be ready + run: | + RETRIES=10 + for i in `seq 1 $RETRIES`; do + echo "Checking health... attempt $i" + status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health || true) + if [ "$status" == "200" ]; then + echo "✅ Health check passed!" + exit 0 + fi + sleep 5 + done + echo "❌ Health check failed after $RETRIES retries." + docker compose logs + exit 1 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag and push image + run: | + # make sure all values are lowercase for repository image name + registry="$(echo $REGISTRY | tr '[:upper:]' '[:lower:]')" + repo_owner="$(echo $REPO_OWNER | tr '[:upper:]' '[:lower:]')" + repo_name="$(echo $REPO_NAME | tr '[:upper:]' '[:lower:]')" + service="$(echo $SERVICE | tr '[:upper:]' '[:lower:]')" + tag="$(echo $TAG | tr '[:upper:]' '[:lower:]')" + + image_name="$repo_name-$service" + source_image="$image_name:latest" + target_image="$registry/$repo_owner/$image_name:$tag" + + echo "source_image: $source_image" + echo "target_image: $target_image" + + docker tag $source_image $target_image + docker push $target_image + + - name: Clean up + run: docker compose down diff --git a/.github/workflows/lint.yml b/.github/workflows/api-lint.yml similarity index 50% rename from .github/workflows/lint.yml rename to .github/workflows/api-lint.yml index d35896c..2ef2296 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/api-lint.yml @@ -1,4 +1,4 @@ -name: Lint +name: Lint API service on: push: @@ -7,26 +7,26 @@ on: pull_request: branches: - main - + workflow_dispatch: jobs: - black: - name: Run black code formatter + ruff: + name: Run ruff code formatter runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 - name: Set up Python uses: actions/setup-python@v5 with: - python-version-file: ".python-version" + python-version-file: "api/.python-version" - name: Install the project - run: uv sync --only-dev + run: uv sync --only-dev --directory api - - name: Run Black Check - run: uv run black --check . + - name: Run Ruff Check + run: uvx --directory api ruff check . diff --git a/.github/workflows/test.yml b/.github/workflows/api-test.yml similarity index 58% rename from .github/workflows/test.yml rename to .github/workflows/api-test.yml index 5b8b612..576eb28 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/api-test.yml @@ -1,4 +1,4 @@ -name: Test +name: Test API service on: push: @@ -7,6 +7,7 @@ on: pull_request: branches: - main + workflow_dispatch: jobs: test: @@ -17,23 +18,24 @@ jobs: - uses: actions/checkout@v4 - name: Install uv - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 - name: Set up Python uses: actions/setup-python@v5 with: - python-version-file: ".python-version" - + python-version-file: "api/.python-version" + - name: Install the project - run: uv sync --all-extras --dev + run: uv sync --all-extras --dev --directory api - name: "Run tests" run: | set -o pipefail - uv run pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=src tests/ | tee pytest-coverage.txt + uv run --directory api pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=src tests/ | tee pytest-coverage.txt - name: "Pytest coverage comment" uses: MishaKav/pytest-coverage-comment@main + if: github.actor != 'dependabot[bot]' with: pytest-coverage-path: ./pytest-coverage.txt - junitxml-path: ./pytest.xml + junitxml-path: ./api/pytest.xml diff --git a/.github/workflows/db-test.yml b/.github/workflows/db-test.yml new file mode 100644 index 0000000..8dee2f9 --- /dev/null +++ b/.github/workflows/db-test.yml @@ -0,0 +1,93 @@ +name: Test DB service + +on: + push: + branches: [ main ] + pull_request: + workflow_dispatch: + +jobs: + test-database: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Start PostgreSQL database + run: | + docker compose up -d db + # Wait for the database to be ready (using the healthcheck) + attempt=1 + max_attempts=30 + until docker compose ps | grep "db" | grep -q "healthy" || [ $attempt -eq $max_attempts ] + do + echo "Waiting for database to be ready... (attempt $attempt/$max_attempts)" + sleep 2 + attempt=$((attempt+1)) + done + + if [ $attempt -eq $max_attempts ]; then + echo "Database failed to become ready in time" + docker compose logs db + exit 1 + fi + + echo "Database is ready!" + docker compose ps + + - name: Run database initialization check + run: | + # Check if the database initialized correctly + docker exec $(docker compose ps -q db) psql -U postgres -d poc -c "SELECT version(), current_database();" + + # List schemas and tables to verify initialization + docker exec $(docker compose ps -q db) psql -U postgres -d poc -c "SELECT schema_name FROM information_schema.schemata WHERE schema_name = 'poc';" + docker exec $(docker compose ps -q db) psql -U postgres -d poc -c "SELECT table_name FROM information_schema.tables WHERE table_schema = 'poc';" + + - name: Run test_scenario.sql + run: | + # Use -v ON_ERROR_STOP=1 to make psql exit with non-zero status on error + cat database/test/test_scenario.sql | docker exec -i $(docker compose ps -q db) psql -U postgres -d poc -v ON_ERROR_STOP=1 + if [ $? -ne 0 ]; then + echo "❌ test_scenario.sql failed" + exit 1 + fi + echo "✅ test_scenario.sql executed successfully" + + - name: Run read.sql + run: | + cat database/test/read.sql | docker exec -i $(docker compose ps -q db) psql -U postgres -d poc -v ON_ERROR_STOP=1 + if [ $? -ne 0 ]; then + echo "❌ read.sql failed" + exit 1 + fi + echo "✅ read.sql executed successfully" + + - name: Run update.sql + run: | + cat database/test/update.sql | docker exec -i $(docker compose ps -q db) psql -U postgres -d poc -v ON_ERROR_STOP=1 + if [ $? -ne 0 ]; then + echo "❌ update.sql failed" + exit 1 + fi + echo "✅ update.sql executed successfully" + + - name: Run delete.sql + run: | + cat database/test/delete.sql | docker exec -i $(docker compose ps -q db) psql -U postgres -d poc -v ON_ERROR_STOP=1 + if [ $? -ne 0 ]; then + echo "❌ delete.sql failed" + exit 1 + fi + echo "✅ delete.sql executed successfully" + + - name: Database logs on failure + if: failure() + run: docker compose logs db + + - name: Clean up + if: always() + run: docker compose down -v diff --git a/.github/workflows/docker-build-and-push.yml b/.github/workflows/docker-build-and-push.yml deleted file mode 100644 index 16afb8f..0000000 --- a/.github/workflows/docker-build-and-push.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: "Docker" - -on: - schedule: - - cron: '0 3 * * *' # Nightly at 3 AM UTC - push: - branches: - - main - pull_request: - -jobs: - build-test-and-push: - name: "Build and push" - runs-on: ubuntu-latest - - permissions: - contents: read - packages: write # Needed to push to GHCR - - env: - IMAGE_NAME: ghcr.io/${{ github.repository }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Prepare Environment - id: prepare-environment - env: - registry: "ghcr.io" - image_name: ${{ github.repository_owner }}/web-api-poc - run: | - NOW="$(date -u +'%Y%m%dT%H%M%SZ')" - echo "now=$NOW" >> $GITHUB_OUTPUT - echo "$NOW" - - registry_image=$( - echo "$registry/$image_name" | \ - tr '[:upper:]' '[:lower:]' \ - ) - REGISTRY_IMAGE=${registry_image} - echo "registry-image=$REGISTRY_IMAGE" - echo "registry-image=$REGISTRY_IMAGE" >> $GITHUB_OUTPUT - - - - name: Build and start container (as backing service) - run: | - docker compose up --build -d - - - name: Wait for container to be ready - run: | - for i in {1..10}; do - echo "Checking health... attempt $i" - status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:80/health || true) - if [ "$status" == "200" ]; then - echo "✅ Health check passed!" - exit 0 - fi - sleep 5 - done - echo "❌ Health check failed after retries." - docker compose logs - exit 1 - - - name: Tag image - id: tag - run: | - if [[ "${{ github.event_name }}" == "schedule" ]]; then - echo "tag=nightly" >> $GITHUB_OUTPUT - elif [[ "${{ github.event_name }}" == "push" ]]; then - echo "tag=latest" >> $GITHUB_OUTPUT - elif [[ "${{ github.event_name }}" == "pull_request" ]]; then - echo "tag=pr-${{ github.event.pull_request.number }}" >> $GITHUB_OUTPUT - else - echo "tag=unknown" >> $GITHUB_OUTPUT - fi - - - name: Build and tag image - run: | - docker build -t ${{ steps.prepare-environment.outputs.registry-image }}:${{ steps.tag.outputs.tag }} . - - - name: Push to GHCR - run: | - docker push ${{ steps.prepare-environment.outputs.registry-image }}:${{ steps.tag.outputs.tag }} - - - name: Clean up - run: docker compose down - diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..53a49a4 --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,65 @@ +name: "Test service integration" + +on: + schedule: # Scheduled workflows only run on the default branch + - cron: '0 3 * * *' # Nightly at 3 AM UTC + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build-and-test: + name: "run Docker instance with docker compose and run integration tests" + runs-on: ubuntu-latest + + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set ENV vars + shell: bash + run: | + cp example.env .env + + - name: Build and start container (as backing service) + shell: bash + run: | + docker compose up --build --detach + + - name: Wait for container to be ready + shell: bash + run: | + RETRIES=10 + for i in `seq 1 $RETRIES`; do + echo "Checking health... attempt $i" + status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health || true) + if [ "$status" == "200" ]; then + echo "✅ Health check passed!" + exit 0 + fi + sleep 5 + done + echo "❌ Health check failed after $RETRIES retries." + docker compose logs + exit 1 + + - name: Install jq + shell: bash + run: | + sudo apt-get install -y jq + + - name: run integration tests + shell: bash + run: | + ./tests/test-integration.sh + + - name: Clean up + shell: bash + run: | + docker compose down --volumes diff --git a/README.md b/README.md index 12c7679..25363c0 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,65 @@ -# web-api-poc -[![Test Status](https://github.com/RMI/web-api-poc/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/test.yml) -[![Docker](https://github.com/RMI/web-api-poc/actions/workflows/docker-build-and-push.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/docker-build-and-push.yml) -[![Lint](https://github.com/RMI/web-api-poc/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/lint.yml) +# WebAPI and Database Proof-of-Concept (poc) -This project is a proof-of-concept (POC) web API built using the FastAPI library. +[![Test DB service](https://github.com/RMI/web-api-poc/actions/workflows/db-test.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/db-test.yml) -## Set-Up +[![Lint API Service](https://github.com/RMI/web-api-poc/actions/workflows/api-lint.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/api-lint.yml) +[![Test API service](https://github.com/RMI/web-api-poc/actions/workflows/api-test.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/api-test.yml) -### Prerequisites +[![Test service integration](https://github.com/RMI/web-api-poc/actions/workflows/integration-test.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/integration-test.yml) +[![Docker](https://github.com/RMI/web-api-poc/actions/workflows/api-docker-build-and-push.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/api-docker-build-and-push.yml) -This project uses [uv](https://github.com/astral-sh/uv) for environment and dependency management. +This project is a proof-of-concept (POC) web API built using the FastAPI library. It is designed to demonstrate the integration of a web API with a database service, including basic CRUD operations and API key authentication. -To install, follow the [official installation guide](https://github.com/astral-sh/uv?tab=readme-ov-file#installation). +## Running the application ### Setup 1. Clone the Repo -``` +```sh git clone https://github.com/RMI/web-api-poc cd web-api-poc ``` -2. Create and Activate the Virtual Environment -``` -uv venv .venv -source .venv/bin/activate # macOS/Linux +2. Create an `.env` file to store the desired API key, (internal) API port, and DB port. +```sh +cp .env.example .env ``` -3. Install Dependencies -``` -uv sync -``` +### Run the services with docker compose -## Running the API - -### Locally serve the Fast API with: - -``` -uv run main.py -``` - -### Run Fast API in docker container with: - -``` +```sh # build the image docker compose build # run the container -docker compose up +docker compose up --detach # do both -docker compose up --build +docker compose up --detach --build ``` -The API will be accessible at http://localhost. - -## Contributing - -### Dependency Management +The API and API documentation (Swagger) will be accessible at http://localhost:8000. -Dependencies are managed using uv. To add a new library, run: +### Make a request from the API -``` -uv add -``` - -### Testing - -Testing is implemented using the `pytest` library. Run all tests locally with: - -``` -uv run pytest -``` - -Or, you can run specific test suites with: -``` -uv run pytest tests/test_unit.py # to only run unit tests -uv run pytest tests/test_integration.py # to only run integration tests +```sh +curl -X 'GET' \ + 'http://localhost:8000/scenarios' \ + -H 'accept: application/json' \ + -H 'X-API-Key: abc123' ``` -For test-only dependencies, add them using: -``` -uv add --dev -``` +Defaults to the API key "abc123", but an alternate key (matching what is in your `.env` file) can be input and submitted on the page. -### Linting +### Shutdown the docker container -This project follows the [black](https://github.com/psf/black) code formatting standard. Lint code by running: +```sh +docker compose down +# also delete the database volume when shutting down the container +docker compose down --volumes ``` -black path/to/file.py # to lint a single file -black . # to lint the entire directory -``` - -Ensure that your code is properly formatted before submitting a pull request. - -### Deployment -**TODO** ## License This project is licensed under the [MIT License](LICENSE.txt) diff --git a/.python-version b/api/.python-version similarity index 100% rename from .python-version rename to api/.python-version diff --git a/Dockerfile b/api/Dockerfile similarity index 92% rename from Dockerfile rename to api/Dockerfile index 08a4167..e575a56 100644 --- a/Dockerfile +++ b/api/Dockerfile @@ -1,5 +1,5 @@ # Stage 1: Get uv binary -FROM ghcr.io/astral-sh/uv:0.6.10 as uv-builder +FROM ghcr.io/astral-sh/uv:0.6.10 AS uv-builder # Stage 2: Build the app FROM python:3.12.6-slim diff --git a/api/README.md b/api/README.md new file mode 100644 index 0000000..ea7ee97 --- /dev/null +++ b/api/README.md @@ -0,0 +1,104 @@ +# WebAPI and database proof of concept (poc) - API service + +[![Test Status](https://github.com/RMI/web-api-poc/actions/workflows/api-test.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/api-test.yml) +[![Docker](https://github.com/RMI/web-api-poc/actions/workflows/api-docker-build-and-push.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/api-docker-build-and-push.yml) +[![Lint](https://github.com/RMI/web-api-poc/actions/workflows/api-lint.yml/badge.svg?branch=main)](https://github.com/RMI/web-api-poc/actions/workflows/api-lint.yml) + +This directory (`api/`) contains the API service for the WebAPI proof of concept repository (poc). + +NOTE: All commands in this README are written with the assumption that the working directory is `api/`, e.g. you have used `cd api` from the root directory of the poc repo, so that the context is exclusive to the API service. + +## Set-Up + +### Prerequisites + +This project uses [uv](https://github.com/astral-sh/uv) for environment and dependency management. + +To install, follow the [official installation guide](https://github.com/astral-sh/uv?tab=readme-ov-file#installation). + +### Setup + +1. Clone the Repo + +```sh +git clone https://github.com/RMI/web-api-poc +cd web-api-poc/api + +2. Create and Activate the Virtual Environment + +```sh +uv venv .venv +source .venv/bin/activate # macOS/Linux +``` + +3. Install Dependencies + +```sh +uv sync +``` + +## Running the API + +### Locally serve the Fast API with: + +```sh +uv run main.py +``` + +### Run Fast API in docker container with: + +```sh +# build the image +docker build --tag api . + +# run the container in the background +docker run --rm --detach --publish 127.0.0.1:8000:8000 api +``` + +The API will be accessible at http://localhost:8000. + +## Contributing + +### Dependency Management + +Dependencies are managed using uv. To add a new library, run: + +```sh +uv add +``` + +### Testing + +Testing is implemented using the `pytest` library. Run all tests locally with: + +```sh +uv run pytest +``` + +Or, you can run specific test suites with: + +```sh +uv run pytest tests/test_unit.py # to only run unit tests +uv run pytest tests/test_integration.py # to only run integration tests +``` + +For test-only dependencies, add them using: + +```sh +uv add --dev +``` + +### Linting + +This project uses [ruff](https://github.com/astral-sh/ruff) for code formatting and linting. + +Style code by running: +``` sh +uvx ruff format path/to/file.py # to lint a single file +uvx ruff format # to lint the entire directory +``` + +Lint code by running: +```sh +uvx ruff check # check if all files pass the linter, and fix failures +uvx ruff check --fix # this will try to automatically fix linter violations diff --git a/api/main.py b/api/main.py new file mode 100644 index 0000000..4ed2422 --- /dev/null +++ b/api/main.py @@ -0,0 +1,26 @@ +from web_api_poc import create_app +from uvicorn import run +from importlib.metadata import metadata +from os import getenv +from dotenv import load_dotenv + +# import .env settings +load_dotenv() +POC_API_PORT = int(getenv("POC_API_PORT", 8000)) +POC_API_LOG_LEVEL = getenv("POC_API_LOG_LEVEL", "info").lower() + +# Validate log level +valid_log_levels = ["critical", "error", "warning", "info", "debug", "trace"] +if POC_API_LOG_LEVEL not in valid_log_levels: + print(f"Warning: Invalid log level '{POC_API_LOG_LEVEL}'. Defaulting to 'info'.") + POC_API_LOG_LEVEL = "info" + +meta = metadata("web_api_poc") + +app = create_app( + title="POC API", description=meta["summary"], version=meta["version"] +) + +if __name__ == "__main__": + print(f"Starting POC API on port {POC_API_PORT} with log level '{POC_API_LOG_LEVEL}'") + run("main:app", host="0.0.0.0", port=POC_API_PORT, log_level=POC_API_LOG_LEVEL) diff --git a/pyproject.toml b/api/pyproject.toml similarity index 77% rename from pyproject.toml rename to api/pyproject.toml index efd317b..b2c1933 100644 --- a/pyproject.toml +++ b/api/pyproject.toml @@ -1,23 +1,25 @@ [project] -name = "web-api-poc" +name = "web_api_poc" version = "0.1.0" -description = "POC for web API using fastapi" +description = "A proof of concept API (poc)" readme = "README.md" requires-python = ">=3.12" dependencies = [ "dotenv>=0.9.9", "fastapi>=0.115.8", + "psycopg2-binary>=2.9.10", "pydantic>=2.10.6", + "sqlalchemy>=2.0.40", "uvicorn>=0.34.0", ] [dependency-groups] dev = [ - "black>=25.1.0", "httpx>=0.28.1", "pytest>=8.3.4", "pytest-asyncio>=0.25.3", "pytest-cov>=6.0.0", + "ruff>=0.11.9", ] [build-system] diff --git a/pytest.ini b/api/pytest.ini similarity index 100% rename from pytest.ini rename to api/pytest.ini diff --git a/api/src/web_api_poc/__init__.py b/api/src/web_api_poc/__init__.py new file mode 100644 index 0000000..b94a1e8 --- /dev/null +++ b/api/src/web_api_poc/__init__.py @@ -0,0 +1,3 @@ +from .app import create_app + +__all__ = ["create_app"] diff --git a/api/src/web_api_poc/app.py b/api/src/web_api_poc/app.py new file mode 100644 index 0000000..18ef3a0 --- /dev/null +++ b/api/src/web_api_poc/app.py @@ -0,0 +1,54 @@ +from fastapi import FastAPI +from fastapi.responses import RedirectResponse +from fastapi.middleware.cors import CORSMiddleware + +from .routers.health import health_router +from .routers.endpoints import endpoints + + +def create_app(title, description, version): + """Create and configure the FastAPI application.""" + + # Create FastAPI app with metadata + app = FastAPI( + title=title, + description=description, + version=version, + contact={ + "name": "RMI", + "url": "https://github.com/RMI", + }, + license_info={ + "name": "MIT", + "url": "https://github.com/RMI/web-api-poc/blob/main/LICENSE.txt", + }, + ) + + # Configure CORS + origins = [ + "http://localhost", + "http://localhost:3000", + "http://0.0.0.0", + "http://0.0.0.0:3000", + "null", + ] # "null" is necessary for a request from a local file + + app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + ) + + # Root endpoint redirects to docs + @app.get("/") + async def redirect(): + response = RedirectResponse(url="/docs") + return response + + # Include routers + app.include_router(health_router) + app.include_router(endpoints) + + return app diff --git a/src/__init__.py b/api/src/web_api_poc/models/__init__.py similarity index 100% rename from src/__init__.py rename to api/src/web_api_poc/models/__init__.py diff --git a/src/models/health.py b/api/src/web_api_poc/models/health.py similarity index 100% rename from src/models/health.py rename to api/src/web_api_poc/models/health.py diff --git a/api/src/web_api_poc/models/pydantic_models.py b/api/src/web_api_poc/models/pydantic_models.py new file mode 100644 index 0000000..65a3720 --- /dev/null +++ b/api/src/web_api_poc/models/pydantic_models.py @@ -0,0 +1,101 @@ +from pydantic import BaseModel +from typing import Optional +from datetime import datetime + + +# Organization Pydantic Model +class OrganizationBase(BaseModel): + name: str + logo_url: Optional[str] = None + + +class OrganizationCreate(OrganizationBase): + pass + + +class OrganizationResponse(OrganizationBase): + id: int + created_on: datetime + updated_on: datetime + + class Config: + orm_mode = True + + +# Scenario Pydantic Model +class ScenarioBase(BaseModel): + name: str + description: Optional[str] = None + usage: Optional[str] = None + time_horizon: Optional[str] = None + source: Optional[str] = None + nature: Optional[str] = None + target_temperature: Optional[str] = None + organization_id: Optional[int] = None + + +class ScenarioCreate(ScenarioBase): + pass + + +class ScenarioResponse(ScenarioBase): + id: int + created_on: datetime + updated_on: datetime + + class Config: + orm_mode = True + + +# Geographic Coverage Pydantic Model +class GeographicCoverageBase(BaseModel): + name: str + + +class GeographicCoverageCreate(GeographicCoverageBase): + pass + + +class GeographicCoverageResponse(GeographicCoverageBase): + id: int + + class Config: + orm_mode = True + + +# Sector Coverage Pydantic Model +class SectorCoverageBase(BaseModel): + name: str + + +class SectorCoverageCreate(SectorCoverageBase): + pass + + +class SectorCoverageResponse(SectorCoverageBase): + id: int + + class Config: + orm_mode = True + + +# Scenario Geographic Coverage Pydantic Model +class ScenarioGeographicCoverageBase(BaseModel): + scenario_id: int + geographic_coverage_id: int + + +class ScenarioGeographicCoverageResponse(ScenarioGeographicCoverageBase): + class Config: + orm_mode = True + + +# Scenario Sector Coverage Pydantic Model +class ScenarioSectorCoverageBase(BaseModel): + scenario_id: int + sector_coverage_id: int + + +class ScenarioSectorCoverageResponse(ScenarioSectorCoverageBase): + class Config: + orm_mode = True diff --git a/api/src/web_api_poc/models/sql_models.py b/api/src/web_api_poc/models/sql_models.py new file mode 100644 index 0000000..6847c9f --- /dev/null +++ b/api/src/web_api_poc/models/sql_models.py @@ -0,0 +1,134 @@ +from sqlalchemy import ( + Column, + Integer, + String, + Text, + ForeignKey, + DateTime, + func, +) +from sqlalchemy.orm import relationship, declarative_base + +Base = declarative_base() + + +# Organizations Table +class Organization(Base): + __tablename__ = "organizations" + __table_args__ = {"schema": "poc"} + + id = Column(Integer, primary_key=True) + name = Column(String(255), unique=True, nullable=False) + logo_url = Column(Text, nullable=True) + created_on = Column(DateTime, default=func.current_timestamp(), nullable=False) + updated_on = Column( + DateTime, + default=func.current_timestamp(), + onupdate=func.current_timestamp(), + nullable=False, + ) + + # Relationships + scenarios = relationship( + "Scenario", back_populates="organization", cascade="all, delete-orphan" + ) + + +# Scenarios Table +class Scenario(Base): + __tablename__ = "scenarios" + __table_args__ = {"schema": "poc"} + + id = Column(Integer, primary_key=True) + name = Column(String(255), nullable=False) + description = Column(Text, nullable=True) + usage = Column(String(50), nullable=True) + time_horizon = Column(String(50), nullable=True) + source = Column(String(255), nullable=True) + nature = Column(String(50), nullable=True) + target_temperature = Column(String(50), nullable=True) + organization_id = Column( + Integer, ForeignKey("poc.organizations.id", ondelete="CASCADE"), nullable=True + ) + created_on = Column(DateTime, default=func.current_timestamp(), nullable=False) + updated_on = Column( + DateTime, + default=func.current_timestamp(), + onupdate=func.current_timestamp(), + nullable=False, + ) + + # Relationships + organization = relationship("Organization", back_populates="scenarios") + geographic_coverages = relationship( + "GeographicCoverage", + secondary="poc.scenario_geographic_coverage", + back_populates="scenarios", + ) + sector_coverages = relationship( + "SectorCoverage", + secondary="poc.scenario_sector_coverage", + back_populates="scenarios", + ) + + +# Geographic Coverage Table +class GeographicCoverage(Base): + __tablename__ = "geographic_coverage" + __table_args__ = {"schema": "poc"} + + id = Column(Integer, primary_key=True) + name = Column(String(255), unique=True, nullable=False) + + # Relationships + scenarios = relationship( + "Scenario", + secondary="poc.scenario_geographic_coverage", + back_populates="geographic_coverages", + ) + + +# Sector Coverage Table +class SectorCoverage(Base): + __tablename__ = "sector_coverage" + __table_args__ = {"schema": "poc"} + + id = Column(Integer, primary_key=True) + name = Column(String(255), unique=True, nullable=False) + + # Relationships + scenarios = relationship( + "Scenario", + secondary="poc.scenario_sector_coverage", + back_populates="sector_coverages", + ) + + +# Junction Table for Scenario and Geographic Coverage +class ScenarioGeographicCoverage(Base): + __tablename__ = "scenario_geographic_coverage" + __table_args__ = {"schema": "poc"} + + scenario_id = Column( + Integer, ForeignKey("poc.scenarios.id", ondelete="CASCADE"), primary_key=True + ) + geographic_coverage_id = Column( + Integer, + ForeignKey("poc.geographic_coverage.id", ondelete="CASCADE"), + primary_key=True, + ) + + +# Junction Table for Scenario and Sector Coverage +class ScenarioSectorCoverage(Base): + __tablename__ = "scenario_sector_coverage" + __table_args__ = {"schema": "poc"} + + scenario_id = Column( + Integer, ForeignKey("poc.scenarios.id", ondelete="CASCADE"), primary_key=True + ) + sector_coverage_id = Column( + Integer, + ForeignKey("poc.sector_coverage.id", ondelete="CASCADE"), + primary_key=True, + ) diff --git a/src/data/__init__.py b/api/src/web_api_poc/routers/__init__.py similarity index 100% rename from src/data/__init__.py rename to api/src/web_api_poc/routers/__init__.py diff --git a/api/src/web_api_poc/routers/endpoints.py b/api/src/web_api_poc/routers/endpoints.py new file mode 100644 index 0000000..7a52c2b --- /dev/null +++ b/api/src/web_api_poc/routers/endpoints.py @@ -0,0 +1,89 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy import inspect +from sqlalchemy.orm import Session +from sqlalchemy.sql import text +from ..models.sql_models import Scenario, Organization +from ..services.db import get_db, engine + + +endpoints = APIRouter() + + +# Router to get all tables in the database +@endpoints.get("/tables") +def get_tables(db=Depends(get_db)): + return {"tables": get_tables_from_db()} + + +# Function to fetch table names using SQLAlchemy inspect +def get_tables_from_db(): + inspector = inspect(engine) # Use the SQLAlchemy engine to inspect the database + return inspector.get_table_names( + schema="poc" + ) # Get table names in the 'poc' schema + + +# Router to get the entire scenarios table +@endpoints.get("/scenarios") +def get_scenarios(db: Session = Depends(get_db)): + # Query the scenarios + scenarios = db.query(Scenario).all() + if not scenarios: + raise HTTPException(status_code=404, detail="Scenario not found") + return scenarios + + +# Router to get scenarios by scenario_id +@endpoints.get("/scenarios/{scenario_id}") +def get_scenario_by_id(scenario_id: int, db: Session = Depends(get_db)): + # Query the Scenarios table by primary key (scenario_id) + scenario = db.query(Scenario).get(scenario_id) + if not scenario: + raise HTTPException(status_code=404, detail="Scenario not found") + return scenario + + +# Router to get the entire organizations table +@endpoints.get("/organizations") +def get_organization(db: Session = Depends(get_db)): + # Query the organizations + organizations = db.query(Organization).all() + if not organizations: + raise HTTPException(status_code=404, detail="Organization not found") + return organizations + + +# Router to get organization by organization_id +@endpoints.get("/organizations/{organization_id}") +def get_organization_by_id(organization_id: int, db: Session = Depends(get_db)): + # Query the Organizations table by primary key (organization_id) + organization = db.query(Organization).get(organization_id) + if not organization: + raise HTTPException(status_code=404, detail="Organization not found") + return organization + +@endpoints.get("/search", summary="Search organizations by name") +def search_organizations_by_name( + q: str = Query(..., description="Free text search query"), + db: Session = Depends(get_db)): + """ + Search for organizations by name using PostgreSQL full-text search. + """ + if not q: + raise HTTPException(status_code=400, detail="Query parameter 'q' is required.") + + # Perform the full-text search dynamically on the `name` field + query = text(""" + SELECT id, name + FROM poc.organizations + WHERE to_tsvector('english', name) @@ to_tsquery(:query) + ORDER BY ts_rank(to_tsvector('english', name), to_tsquery(:query)) DESC + """) + results = db.execute(query, {"query": q}).fetchall() + + # Format the results + items = [{"id": row.id, "name": row.name} for row in results] + return { + "total_count": len(items), + "items": items + } diff --git a/src/routers/health.py b/api/src/web_api_poc/routers/health.py similarity index 92% rename from src/routers/health.py rename to api/src/web_api_poc/routers/health.py index 46a81bf..e2b50c6 100644 --- a/src/routers/health.py +++ b/api/src/web_api_poc/routers/health.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, status -from models.health import HealthCheck +from ..models.health import HealthCheck health_router = APIRouter() diff --git a/src/models/__init__.py b/api/src/web_api_poc/services/__init__.py similarity index 100% rename from src/models/__init__.py rename to api/src/web_api_poc/services/__init__.py diff --git a/src/services/auth.py b/api/src/web_api_poc/services/auth.py similarity index 50% rename from src/services/auth.py rename to api/src/web_api_poc/services/auth.py index b28f47e..113a7a2 100644 --- a/src/services/auth.py +++ b/api/src/web_api_poc/services/auth.py @@ -6,13 +6,13 @@ # Load environment variables from a .env file load_dotenv() -API_KEY = os.getenv("API_KEY") -API_KEY_NAME = "X-API-Key" +POC_API_KEY = os.getenv("POC_API_KEY") +POC_API_KEY_NAME = "X-API-Key" -api_key_header = APIKeyHeader(name=API_KEY_NAME, auto_error=True) +api_key_header = APIKeyHeader(name=POC_API_KEY_NAME, auto_error=True) -def get_api_key(api_key: str = Security(api_key_header)): - if api_key == API_KEY: - return api_key +def get_api_key(POC_API_KEY: str = Security(api_key_header)): + if POC_API_KEY == POC_API_KEY: + return POC_API_KEY raise HTTPException(status_code=403, detail="Invalid API Key") diff --git a/api/src/web_api_poc/services/db.py b/api/src/web_api_poc/services/db.py new file mode 100644 index 0000000..c28ebd6 --- /dev/null +++ b/api/src/web_api_poc/services/db.py @@ -0,0 +1,22 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from os import getenv +from dotenv import load_dotenv + +# import .env settings +load_dotenv() +POC_DB_PORT = getenv("POC_DB_PORT", "5432") + +# Define database connection string +DATABASE_URL = "postgresql://postgres:postgres@db:" + POC_DB_PORT + "/poc" +# Set up SQLAlchemy engine and session +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +# get database session +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/api/tests/test_server.py b/api/tests/test_server.py new file mode 100644 index 0000000..a8d6ff8 --- /dev/null +++ b/api/tests/test_server.py @@ -0,0 +1,44 @@ +from fastapi.testclient import TestClient +from web_api_poc import create_app +from web_api_poc.services.auth import get_api_key + +app = create_app(title="foo", description="bar", version="baz") + + +def override_get_api_key(): + return True + + +app.dependency_overrides[get_api_key] = override_get_api_key + +base_url = "http://testserver" +client = TestClient(app, base_url=base_url) + + +def test_root_redirects(): + response = client.get("/", follow_redirects=False) + assert response.status_code == 307 # Temporary Redirect + assert response.headers["location"] == "/docs" + + +def test_root_redirect_follows(): + response = client.get("/", follow_redirects=True) + assert response.status_code == 200 + assert "Swagger UI" in response.text + + +def test_health_check(): + response = client.get("/health", follow_redirects=False) + assert response.status_code == 200 + assert response.json() == {"status": "OK"} + + +def test_health_check_redirect(): + response = client.get("/health/", follow_redirects=False) + assert response.status_code == 307 + assert response.headers["location"] == base_url + "/health" + + +def test_bad_request(): + response = client.get("/xxx") + assert response.status_code == 404 diff --git a/tests/test_unit.py b/api/tests/test_unit.py similarity index 72% rename from tests/test_unit.py rename to api/tests/test_unit.py index 9e0a23a..ea37e3e 100644 --- a/tests/test_unit.py +++ b/api/tests/test_unit.py @@ -1,5 +1,5 @@ import pytest -from routers.health import get_health +from web_api_poc.routers.health import get_health @pytest.mark.asyncio diff --git a/api/uv.lock b/api/uv.lock new file mode 100644 index 0000000..7364386 --- /dev/null +++ b/api/uv.lock @@ -0,0 +1,516 @@ +version = 1 +requires-python = ">=3.12" + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 }, +] + +[[package]] +name = "click" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "coverage" +version = "7.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/07/998afa4a0ecdf9b1981ae05415dad2d4e7716e1b1f00abbd91691ac09ac9/coverage-7.8.2.tar.gz", hash = "sha256:a886d531373a1f6ff9fad2a2ba4a045b68467b779ae729ee0b3b10ac20033b27", size = 812759 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/2a/1da1ada2e3044fcd4a3254fb3576e160b8fe5b36d705c8a31f793423f763/coverage-7.8.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e2f6fe3654468d061942591aef56686131335b7a8325684eda85dacdf311356c", size = 211876 }, + { url = "https://files.pythonhosted.org/packages/70/e9/3d715ffd5b6b17a8be80cd14a8917a002530a99943cc1939ad5bb2aa74b9/coverage-7.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76090fab50610798cc05241bf83b603477c40ee87acd358b66196ab0ca44ffa1", size = 212130 }, + { url = "https://files.pythonhosted.org/packages/a0/02/fdce62bb3c21649abfd91fbdcf041fb99be0d728ff00f3f9d54d97ed683e/coverage-7.8.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2bd0a0a5054be160777a7920b731a0570284db5142abaaf81bcbb282b8d99279", size = 246176 }, + { url = "https://files.pythonhosted.org/packages/a7/52/decbbed61e03b6ffe85cd0fea360a5e04a5a98a7423f292aae62423b8557/coverage-7.8.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da23ce9a3d356d0affe9c7036030b5c8f14556bd970c9b224f9c8205505e3b99", size = 243068 }, + { url = "https://files.pythonhosted.org/packages/38/6c/d0e9c0cce18faef79a52778219a3c6ee8e336437da8eddd4ab3dbd8fadff/coverage-7.8.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9392773cffeb8d7e042a7b15b82a414011e9d2b5fdbbd3f7e6a6b17d5e21b20", size = 245328 }, + { url = "https://files.pythonhosted.org/packages/f0/70/f703b553a2f6b6c70568c7e398ed0789d47f953d67fbba36a327714a7bca/coverage-7.8.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:876cbfd0b09ce09d81585d266c07a32657beb3eaec896f39484b631555be0fe2", size = 245099 }, + { url = "https://files.pythonhosted.org/packages/ec/fb/4cbb370dedae78460c3aacbdad9d249e853f3bc4ce5ff0e02b1983d03044/coverage-7.8.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3da9b771c98977a13fbc3830f6caa85cae6c9c83911d24cb2d218e9394259c57", size = 243314 }, + { url = "https://files.pythonhosted.org/packages/39/9f/1afbb2cb9c8699b8bc38afdce00a3b4644904e6a38c7bf9005386c9305ec/coverage-7.8.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a990f6510b3292686713bfef26d0049cd63b9c7bb17e0864f133cbfd2e6167f", size = 244489 }, + { url = "https://files.pythonhosted.org/packages/79/fa/f3e7ec7d220bff14aba7a4786ae47043770cbdceeea1803083059c878837/coverage-7.8.2-cp312-cp312-win32.whl", hash = "sha256:bf8111cddd0f2b54d34e96613e7fbdd59a673f0cf5574b61134ae75b6f5a33b8", size = 214366 }, + { url = "https://files.pythonhosted.org/packages/54/aa/9cbeade19b7e8e853e7ffc261df885d66bf3a782c71cba06c17df271f9e6/coverage-7.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:86a323a275e9e44cdf228af9b71c5030861d4d2610886ab920d9945672a81223", size = 215165 }, + { url = "https://files.pythonhosted.org/packages/c4/73/e2528bf1237d2448f882bbebaec5c3500ef07301816c5c63464b9da4d88a/coverage-7.8.2-cp312-cp312-win_arm64.whl", hash = "sha256:820157de3a589e992689ffcda8639fbabb313b323d26388d02e154164c57b07f", size = 213548 }, + { url = "https://files.pythonhosted.org/packages/1a/93/eb6400a745ad3b265bac36e8077fdffcf0268bdbbb6c02b7220b624c9b31/coverage-7.8.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ea561010914ec1c26ab4188aef8b1567272ef6de096312716f90e5baa79ef8ca", size = 211898 }, + { url = "https://files.pythonhosted.org/packages/1b/7c/bdbf113f92683024406a1cd226a199e4200a2001fc85d6a6e7e299e60253/coverage-7.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cb86337a4fcdd0e598ff2caeb513ac604d2f3da6d53df2c8e368e07ee38e277d", size = 212171 }, + { url = "https://files.pythonhosted.org/packages/91/22/594513f9541a6b88eb0dba4d5da7d71596dadef6b17a12dc2c0e859818a9/coverage-7.8.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26a4636ddb666971345541b59899e969f3b301143dd86b0ddbb570bd591f1e85", size = 245564 }, + { url = "https://files.pythonhosted.org/packages/1f/f4/2860fd6abeebd9f2efcfe0fd376226938f22afc80c1943f363cd3c28421f/coverage-7.8.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5040536cf9b13fb033f76bcb5e1e5cb3b57c4807fef37db9e0ed129c6a094257", size = 242719 }, + { url = "https://files.pythonhosted.org/packages/89/60/f5f50f61b6332451520e6cdc2401700c48310c64bc2dd34027a47d6ab4ca/coverage-7.8.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc67994df9bcd7e0150a47ef41278b9e0a0ea187caba72414b71dc590b99a108", size = 244634 }, + { url = "https://files.pythonhosted.org/packages/3b/70/7f4e919039ab7d944276c446b603eea84da29ebcf20984fb1fdf6e602028/coverage-7.8.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e6c86888fd076d9e0fe848af0a2142bf606044dc5ceee0aa9eddb56e26895a0", size = 244824 }, + { url = "https://files.pythonhosted.org/packages/26/45/36297a4c0cea4de2b2c442fe32f60c3991056c59cdc3cdd5346fbb995c97/coverage-7.8.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:684ca9f58119b8e26bef860db33524ae0365601492e86ba0b71d513f525e7050", size = 242872 }, + { url = "https://files.pythonhosted.org/packages/a4/71/e041f1b9420f7b786b1367fa2a375703889ef376e0d48de9f5723fb35f11/coverage-7.8.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8165584ddedb49204c4e18da083913bdf6a982bfb558632a79bdaadcdafd0d48", size = 244179 }, + { url = "https://files.pythonhosted.org/packages/bd/db/3c2bf49bdc9de76acf2491fc03130c4ffc51469ce2f6889d2640eb563d77/coverage-7.8.2-cp313-cp313-win32.whl", hash = "sha256:34759ee2c65362163699cc917bdb2a54114dd06d19bab860725f94ef45a3d9b7", size = 214393 }, + { url = "https://files.pythonhosted.org/packages/c6/dc/947e75d47ebbb4b02d8babb1fad4ad381410d5bc9da7cfca80b7565ef401/coverage-7.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:2f9bc608fbafaee40eb60a9a53dbfb90f53cc66d3d32c2849dc27cf5638a21e3", size = 215194 }, + { url = "https://files.pythonhosted.org/packages/90/31/a980f7df8a37eaf0dc60f932507fda9656b3a03f0abf188474a0ea188d6d/coverage-7.8.2-cp313-cp313-win_arm64.whl", hash = "sha256:9fe449ee461a3b0c7105690419d0b0aba1232f4ff6d120a9e241e58a556733f7", size = 213580 }, + { url = "https://files.pythonhosted.org/packages/8a/6a/25a37dd90f6c95f59355629417ebcb74e1c34e38bb1eddf6ca9b38b0fc53/coverage-7.8.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8369a7c8ef66bded2b6484053749ff220dbf83cba84f3398c84c51a6f748a008", size = 212734 }, + { url = "https://files.pythonhosted.org/packages/36/8b/3a728b3118988725f40950931abb09cd7f43b3c740f4640a59f1db60e372/coverage-7.8.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:159b81df53a5fcbc7d45dae3adad554fdbde9829a994e15227b3f9d816d00b36", size = 212959 }, + { url = "https://files.pythonhosted.org/packages/53/3c/212d94e6add3a3c3f412d664aee452045ca17a066def8b9421673e9482c4/coverage-7.8.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6fcbbd35a96192d042c691c9e0c49ef54bd7ed865846a3c9d624c30bb67ce46", size = 257024 }, + { url = "https://files.pythonhosted.org/packages/a4/40/afc03f0883b1e51bbe804707aae62e29c4e8c8bbc365c75e3e4ddeee9ead/coverage-7.8.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05364b9cc82f138cc86128dc4e2e1251c2981a2218bfcd556fe6b0fbaa3501be", size = 252867 }, + { url = "https://files.pythonhosted.org/packages/18/a2/3699190e927b9439c6ded4998941a3c1d6fa99e14cb28d8536729537e307/coverage-7.8.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46d532db4e5ff3979ce47d18e2fe8ecad283eeb7367726da0e5ef88e4fe64740", size = 255096 }, + { url = "https://files.pythonhosted.org/packages/b4/06/16e3598b9466456b718eb3e789457d1a5b8bfb22e23b6e8bbc307df5daf0/coverage-7.8.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4000a31c34932e7e4fa0381a3d6deb43dc0c8f458e3e7ea6502e6238e10be625", size = 256276 }, + { url = "https://files.pythonhosted.org/packages/a7/d5/4b5a120d5d0223050a53d2783c049c311eea1709fa9de12d1c358e18b707/coverage-7.8.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:43ff5033d657cd51f83015c3b7a443287250dc14e69910577c3e03bd2e06f27b", size = 254478 }, + { url = "https://files.pythonhosted.org/packages/ba/85/f9ecdb910ecdb282b121bfcaa32fa8ee8cbd7699f83330ee13ff9bbf1a85/coverage-7.8.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94316e13f0981cbbba132c1f9f365cac1d26716aaac130866ca812006f662199", size = 255255 }, + { url = "https://files.pythonhosted.org/packages/50/63/2d624ac7d7ccd4ebbd3c6a9eba9d7fc4491a1226071360d59dd84928ccb2/coverage-7.8.2-cp313-cp313t-win32.whl", hash = "sha256:3f5673888d3676d0a745c3d0e16da338c5eea300cb1f4ada9c872981265e76d8", size = 215109 }, + { url = "https://files.pythonhosted.org/packages/22/5e/7053b71462e970e869111c1853afd642212568a350eba796deefdfbd0770/coverage-7.8.2-cp313-cp313t-win_amd64.whl", hash = "sha256:2c08b05ee8d7861e45dc5a2cc4195c8c66dca5ac613144eb6ebeaff2d502e73d", size = 216268 }, + { url = "https://files.pythonhosted.org/packages/07/69/afa41aa34147655543dbe96994f8a246daf94b361ccf5edfd5df62ce066a/coverage-7.8.2-cp313-cp313t-win_arm64.whl", hash = "sha256:1e1448bb72b387755e1ff3ef1268a06617afd94188164960dba8d0245a46004b", size = 214071 }, + { url = "https://files.pythonhosted.org/packages/a0/1a/0b9c32220ad694d66062f571cc5cedfa9997b64a591e8a500bb63de1bd40/coverage-7.8.2-py3-none-any.whl", hash = "sha256:726f32ee3713f7359696331a18daf0c3b3a70bb0ae71141b9d3c52be7c595e32", size = 203623 }, +] + +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 }, +] + +[[package]] +name = "fastapi" +version = "0.115.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, +] + +[[package]] +name = "greenlet" +version = "3.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/c1/a82edae11d46c0d83481aacaa1e578fea21d94a1ef400afd734d47ad95ad/greenlet-3.2.2.tar.gz", hash = "sha256:ad053d34421a2debba45aa3cc39acf454acbcd025b3fc1a9f8a0dee237abd485", size = 185797 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/a1/88fdc6ce0df6ad361a30ed78d24c86ea32acb2b563f33e39e927b1da9ea0/greenlet-3.2.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:df4d1509efd4977e6a844ac96d8be0b9e5aa5d5c77aa27ca9f4d3f92d3fcf330", size = 270413 }, + { url = "https://files.pythonhosted.org/packages/a6/2e/6c1caffd65490c68cd9bcec8cb7feb8ac7b27d38ba1fea121fdc1f2331dc/greenlet-3.2.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da956d534a6d1b9841f95ad0f18ace637668f680b1339ca4dcfb2c1837880a0b", size = 637242 }, + { url = "https://files.pythonhosted.org/packages/98/28/088af2cedf8823b6b7ab029a5626302af4ca1037cf8b998bed3a8d3cb9e2/greenlet-3.2.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c7b15fb9b88d9ee07e076f5a683027bc3befd5bb5d25954bb633c385d8b737e", size = 651444 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0116ab876bb0bc7a81eadc21c3f02cd6100dcd25a1cf2a085a130a63a26a/greenlet-3.2.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752f0e79785e11180ebd2e726c8a88109ded3e2301d40abced2543aa5d164275", size = 646067 }, + { url = "https://files.pythonhosted.org/packages/35/17/bb8f9c9580e28a94a9575da847c257953d5eb6e39ca888239183320c1c28/greenlet-3.2.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae572c996ae4b5e122331e12bbb971ea49c08cc7c232d1bd43150800a2d6c65", size = 648153 }, + { url = "https://files.pythonhosted.org/packages/2c/ee/7f31b6f7021b8df6f7203b53b9cc741b939a2591dcc6d899d8042fcf66f2/greenlet-3.2.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02f5972ff02c9cf615357c17ab713737cccfd0eaf69b951084a9fd43f39833d3", size = 603865 }, + { url = "https://files.pythonhosted.org/packages/b5/2d/759fa59323b521c6f223276a4fc3d3719475dc9ae4c44c2fe7fc750f8de0/greenlet-3.2.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4fefc7aa68b34b9224490dfda2e70ccf2131368493add64b4ef2d372955c207e", size = 1119575 }, + { url = "https://files.pythonhosted.org/packages/30/05/356813470060bce0e81c3df63ab8cd1967c1ff6f5189760c1a4734d405ba/greenlet-3.2.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a31ead8411a027c2c4759113cf2bd473690517494f3d6e4bf67064589afcd3c5", size = 1147460 }, + { url = "https://files.pythonhosted.org/packages/07/f4/b2a26a309a04fb844c7406a4501331b9400e1dd7dd64d3450472fd47d2e1/greenlet-3.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b24c7844c0a0afc3ccbeb0b807adeefb7eff2b5599229ecedddcfeb0ef333bec", size = 296239 }, + { url = "https://files.pythonhosted.org/packages/89/30/97b49779fff8601af20972a62cc4af0c497c1504dfbb3e93be218e093f21/greenlet-3.2.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:3ab7194ee290302ca15449f601036007873028712e92ca15fc76597a0aeb4c59", size = 269150 }, + { url = "https://files.pythonhosted.org/packages/21/30/877245def4220f684bc2e01df1c2e782c164e84b32e07373992f14a2d107/greenlet-3.2.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dc5c43bb65ec3669452af0ab10729e8fdc17f87a1f2ad7ec65d4aaaefabf6bf", size = 637381 }, + { url = "https://files.pythonhosted.org/packages/8e/16/adf937908e1f913856b5371c1d8bdaef5f58f251d714085abeea73ecc471/greenlet-3.2.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:decb0658ec19e5c1f519faa9a160c0fc85a41a7e6654b3ce1b44b939f8bf1325", size = 651427 }, + { url = "https://files.pythonhosted.org/packages/ad/49/6d79f58fa695b618654adac64e56aff2eeb13344dc28259af8f505662bb1/greenlet-3.2.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6fadd183186db360b61cb34e81117a096bff91c072929cd1b529eb20dd46e6c5", size = 645795 }, + { url = "https://files.pythonhosted.org/packages/5a/e6/28ed5cb929c6b2f001e96b1d0698c622976cd8f1e41fe7ebc047fa7c6dd4/greenlet-3.2.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1919cbdc1c53ef739c94cf2985056bcc0838c1f217b57647cbf4578576c63825", size = 648398 }, + { url = "https://files.pythonhosted.org/packages/9d/70/b200194e25ae86bc57077f695b6cc47ee3118becf54130c5514456cf8dac/greenlet-3.2.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3885f85b61798f4192d544aac7b25a04ece5fe2704670b4ab73c2d2c14ab740d", size = 606795 }, + { url = "https://files.pythonhosted.org/packages/f8/c8/ba1def67513a941154ed8f9477ae6e5a03f645be6b507d3930f72ed508d3/greenlet-3.2.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:85f3e248507125bf4af607a26fd6cb8578776197bd4b66e35229cdf5acf1dfbf", size = 1117976 }, + { url = "https://files.pythonhosted.org/packages/c3/30/d0e88c1cfcc1b3331d63c2b54a0a3a4a950ef202fb8b92e772ca714a9221/greenlet-3.2.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:1e76106b6fc55fa3d6fe1c527f95ee65e324a13b62e243f77b48317346559708", size = 1145509 }, + { url = "https://files.pythonhosted.org/packages/90/2e/59d6491834b6e289051b252cf4776d16da51c7c6ca6a87ff97e3a50aa0cd/greenlet-3.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:fe46d4f8e94e637634d54477b0cfabcf93c53f29eedcbdeecaf2af32029b4421", size = 296023 }, + { url = "https://files.pythonhosted.org/packages/65/66/8a73aace5a5335a1cba56d0da71b7bd93e450f17d372c5b7c5fa547557e9/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba30e88607fb6990544d84caf3c706c4b48f629e18853fc6a646f82db9629418", size = 629911 }, + { url = "https://files.pythonhosted.org/packages/48/08/c8b8ebac4e0c95dcc68ec99198842e7db53eda4ab3fb0a4e785690883991/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:055916fafad3e3388d27dd68517478933a97edc2fc54ae79d3bec827de2c64c4", size = 635251 }, + { url = "https://files.pythonhosted.org/packages/37/26/7db30868f73e86b9125264d2959acabea132b444b88185ba5c462cb8e571/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2593283bf81ca37d27d110956b79e8723f9aa50c4bcdc29d3c0543d4743d2763", size = 632620 }, + { url = "https://files.pythonhosted.org/packages/10/ec/718a3bd56249e729016b0b69bee4adea0dfccf6ca43d147ef3b21edbca16/greenlet-3.2.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89c69e9a10670eb7a66b8cef6354c24671ba241f46152dd3eed447f79c29fb5b", size = 628851 }, + { url = "https://files.pythonhosted.org/packages/9b/9d/d1c79286a76bc62ccdc1387291464af16a4204ea717f24e77b0acd623b99/greenlet-3.2.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02a98600899ca1ca5d3a2590974c9e3ec259503b2d6ba6527605fcd74e08e207", size = 593718 }, + { url = "https://files.pythonhosted.org/packages/cd/41/96ba2bf948f67b245784cd294b84e3d17933597dffd3acdb367a210d1949/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:b50a8c5c162469c3209e5ec92ee4f95c8231b11db6a04db09bbe338176723bb8", size = 1105752 }, + { url = "https://files.pythonhosted.org/packages/68/3b/3b97f9d33c1f2eb081759da62bd6162159db260f602f048bc2f36b4c453e/greenlet-3.2.2-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:45f9f4853fb4cc46783085261c9ec4706628f3b57de3e68bae03e8f8b3c0de51", size = 1125170 }, + { url = "https://files.pythonhosted.org/packages/31/df/b7d17d66c8d0f578d2885a3d8f565e9e4725eacc9d3fdc946d0031c055c4/greenlet-3.2.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:9ea5231428af34226c05f927e16fc7f6fa5e39e3ad3cd24ffa48ba53a47f4240", size = 269899 }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "psycopg2-binary" +version = "2.9.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771 }, + { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336 }, + { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637 }, + { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097 }, + { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776 }, + { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968 }, + { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334 }, + { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722 }, + { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132 }, + { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312 }, + { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191 }, + { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031 }, + { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699 }, + { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245 }, + { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631 }, + { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140 }, + { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762 }, + { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967 }, + { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326 }, + { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712 }, + { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155 }, + { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356 }, + { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 }, +] + +[[package]] +name = "pydantic" +version = "2.11.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f0/86/8ce9040065e8f924d642c58e4a344e33163a07f6b57f836d0d734e0ad3fb/pydantic-2.11.5.tar.gz", hash = "sha256:7f853db3d0ce78ce8bbb148c401c2cdd6431b3473c0cdff2755c7690952a7b7a", size = 787102 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/69/831ed22b38ff9b4b64b66569f0e5b7b97cf3638346eb95a2147fdb49ad5f/pydantic-2.11.5-py3-none-any.whl", hash = "sha256:f9c26ba06f9747749ca1e5c94d6a85cb84254577553c8785576fd38fa64dc0f7", size = 444229 }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000 }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996 }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957 }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199 }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296 }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109 }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028 }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044 }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881 }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034 }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187 }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628 }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866 }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894 }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688 }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808 }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580 }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859 }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810 }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498 }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611 }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924 }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196 }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389 }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223 }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473 }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269 }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921 }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162 }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560 }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976 }, +] + +[[package]] +name = "pytest-cov" +version = "6.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, +] + +[[package]] +name = "python-dotenv" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/2c/7bb1416c5620485aa793f2de31d3df393d3686aa8a8506d11e10e13c5baf/python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5", size = 39920 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/18/98a99ad95133c6a6e2005fe89faedf294a748bd5dc803008059409ac9b1e/python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d", size = 20256 }, +] + +[[package]] +name = "ruff" +version = "0.11.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049 }, + { url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601 }, + { url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421 }, + { url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980 }, + { url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241 }, + { url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398 }, + { url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955 }, + { url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803 }, + { url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630 }, + { url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310 }, + { url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144 }, + { url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987 }, + { url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922 }, + { url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537 }, + { url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492 }, + { url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562 }, + { url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "sqlalchemy" +version = "2.0.41" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "(python_full_version < '3.14' and platform_machine == 'AMD64') or (python_full_version < '3.14' and platform_machine == 'WIN32') or (python_full_version < '3.14' and platform_machine == 'aarch64') or (python_full_version < '3.14' and platform_machine == 'amd64') or (python_full_version < '3.14' and platform_machine == 'ppc64le') or (python_full_version < '3.14' and platform_machine == 'win32') or (python_full_version < '3.14' and platform_machine == 'x86_64')" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/66/45b165c595ec89aa7dcc2c1cd222ab269bc753f1fc7a1e68f8481bd957bf/sqlalchemy-2.0.41.tar.gz", hash = "sha256:edba70118c4be3c2b1f90754d308d0b79c6fe2c0fdc52d8ddf603916f83f4db9", size = 9689424 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/2a/f1f4e068b371154740dd10fb81afb5240d5af4aa0087b88d8b308b5429c2/sqlalchemy-2.0.41-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:81f413674d85cfd0dfcd6512e10e0f33c19c21860342a4890c3a2b59479929f9", size = 2119645 }, + { url = "https://files.pythonhosted.org/packages/9b/e8/c664a7e73d36fbfc4730f8cf2bf930444ea87270f2825efbe17bf808b998/sqlalchemy-2.0.41-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:598d9ebc1e796431bbd068e41e4de4dc34312b7aa3292571bb3674a0cb415dd1", size = 2107399 }, + { url = "https://files.pythonhosted.org/packages/5c/78/8a9cf6c5e7135540cb682128d091d6afa1b9e48bd049b0d691bf54114f70/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a104c5694dfd2d864a6f91b0956eb5d5883234119cb40010115fd45a16da5e70", size = 3293269 }, + { url = "https://files.pythonhosted.org/packages/3c/35/f74add3978c20de6323fb11cb5162702670cc7a9420033befb43d8d5b7a4/sqlalchemy-2.0.41-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6145afea51ff0af7f2564a05fa95eb46f542919e6523729663a5d285ecb3cf5e", size = 3303364 }, + { url = "https://files.pythonhosted.org/packages/6a/d4/c990f37f52c3f7748ebe98883e2a0f7d038108c2c5a82468d1ff3eec50b7/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b46fa6eae1cd1c20e6e6f44e19984d438b6b2d8616d21d783d150df714f44078", size = 3229072 }, + { url = "https://files.pythonhosted.org/packages/15/69/cab11fecc7eb64bc561011be2bd03d065b762d87add52a4ca0aca2e12904/sqlalchemy-2.0.41-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41836fe661cc98abfae476e14ba1906220f92c4e528771a8a3ae6a151242d2ae", size = 3268074 }, + { url = "https://files.pythonhosted.org/packages/5c/ca/0c19ec16858585d37767b167fc9602593f98998a68a798450558239fb04a/sqlalchemy-2.0.41-cp312-cp312-win32.whl", hash = "sha256:a8808d5cf866c781150d36a3c8eb3adccfa41a8105d031bf27e92c251e3969d6", size = 2084514 }, + { url = "https://files.pythonhosted.org/packages/7f/23/4c2833d78ff3010a4e17f984c734f52b531a8c9060a50429c9d4b0211be6/sqlalchemy-2.0.41-cp312-cp312-win_amd64.whl", hash = "sha256:5b14e97886199c1f52c14629c11d90c11fbb09e9334fa7bb5f6d068d9ced0ce0", size = 2111557 }, + { url = "https://files.pythonhosted.org/packages/d3/ad/2e1c6d4f235a97eeef52d0200d8ddda16f6c4dd70ae5ad88c46963440480/sqlalchemy-2.0.41-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4eeb195cdedaf17aab6b247894ff2734dcead6c08f748e617bfe05bd5a218443", size = 2115491 }, + { url = "https://files.pythonhosted.org/packages/cf/8d/be490e5db8400dacc89056f78a52d44b04fbf75e8439569d5b879623a53b/sqlalchemy-2.0.41-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d4ae769b9c1c7757e4ccce94b0641bc203bbdf43ba7a2413ab2523d8d047d8dc", size = 2102827 }, + { url = "https://files.pythonhosted.org/packages/a0/72/c97ad430f0b0e78efaf2791342e13ffeafcbb3c06242f01a3bb8fe44f65d/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a62448526dd9ed3e3beedc93df9bb6b55a436ed1474db31a2af13b313a70a7e1", size = 3225224 }, + { url = "https://files.pythonhosted.org/packages/5e/51/5ba9ea3246ea068630acf35a6ba0d181e99f1af1afd17e159eac7e8bc2b8/sqlalchemy-2.0.41-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc56c9788617b8964ad02e8fcfeed4001c1f8ba91a9e1f31483c0dffb207002a", size = 3230045 }, + { url = "https://files.pythonhosted.org/packages/78/2f/8c14443b2acea700c62f9b4a8bad9e49fc1b65cfb260edead71fd38e9f19/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c153265408d18de4cc5ded1941dcd8315894572cddd3c58df5d5b5705b3fa28d", size = 3159357 }, + { url = "https://files.pythonhosted.org/packages/fc/b2/43eacbf6ccc5276d76cea18cb7c3d73e294d6fb21f9ff8b4eef9b42bbfd5/sqlalchemy-2.0.41-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4f67766965996e63bb46cfbf2ce5355fc32d9dd3b8ad7e536a920ff9ee422e23", size = 3197511 }, + { url = "https://files.pythonhosted.org/packages/fa/2e/677c17c5d6a004c3c45334ab1dbe7b7deb834430b282b8a0f75ae220c8eb/sqlalchemy-2.0.41-cp313-cp313-win32.whl", hash = "sha256:bfc9064f6658a3d1cadeaa0ba07570b83ce6801a1314985bf98ec9b95d74e15f", size = 2082420 }, + { url = "https://files.pythonhosted.org/packages/e9/61/e8c1b9b6307c57157d328dd8b8348ddc4c47ffdf1279365a13b2b98b8049/sqlalchemy-2.0.41-cp313-cp313-win_amd64.whl", hash = "sha256:82ca366a844eb551daff9d2e6e7a9e5e76d2612c8564f58db6c19a726869c1df", size = 2108329 }, + { url = "https://files.pythonhosted.org/packages/1c/fc/9ba22f01b5cdacc8f5ed0d22304718d2c758fce3fd49a5372b886a86f37c/sqlalchemy-2.0.41-py3-none-any.whl", hash = "sha256:57df5dc6fdb5ed1a88a1ed2195fd31927e705cad62dedd86b46972752a80f576", size = 1911224 }, +] + +[[package]] +name = "starlette" +version = "0.46.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552 }, +] + +[[package]] +name = "uvicorn" +version = "0.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/ae/9bbb19b9e1c450cf9ecaef06463e40234d98d95bf572fab11b4f19ae5ded/uvicorn-0.34.2.tar.gz", hash = "sha256:0e929828f6186353a80b58ea719861d2629d766293b6d19baf086ba31d4f3328", size = 76815 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/4b/4cef6ce21a2aaca9d852a6e84ef4f135d99fcd74fa75105e2fc0c8308acd/uvicorn-0.34.2-py3-none-any.whl", hash = "sha256:deb49af569084536d269fe0a6d67e3754f104cf03aba7c11c40f01aadf33c403", size = 62483 }, +] + +[[package]] +name = "web-api-poc" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "dotenv" }, + { name = "fastapi" }, + { name = "psycopg2-binary" }, + { name = "pydantic" }, + { name = "sqlalchemy" }, + { name = "uvicorn" }, +] + +[package.dev-dependencies] +dev = [ + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "pytest-cov" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "fastapi", specifier = ">=0.115.8" }, + { name = "psycopg2-binary", specifier = ">=2.9.10" }, + { name = "pydantic", specifier = ">=2.10.6" }, + { name = "sqlalchemy", specifier = ">=2.0.40" }, + { name = "uvicorn", specifier = ">=0.34.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pytest", specifier = ">=8.3.4" }, + { name = "pytest-asyncio", specifier = ">=0.25.3" }, + { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "ruff", specifier = ">=0.11.9" }, +] diff --git a/database/init/pbtar-schemal.sql b/database/init/pbtar-schemal.sql new file mode 100644 index 0000000..00fa456 --- /dev/null +++ b/database/init/pbtar-schemal.sql @@ -0,0 +1,171 @@ +-- Create schema +CREATE SCHEMA IF NOT EXISTS poc; + +-- Set search path +SET search_path TO poc; + +-- Create table for organizations +CREATE TABLE IF NOT EXISTS organizations ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL, + logo_url TEXT, + created_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create table for scenarios +CREATE TABLE IF NOT EXISTS scenarios ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + usage VARCHAR(50), + time_horizon VARCHAR(50), + source VARCHAR(255), + nature VARCHAR(50), + target_temperature VARCHAR(50), + organization_id INTEGER REFERENCES organizations(id) ON DELETE CASCADE, + created_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_on TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Create table for geographic coverage +CREATE TABLE IF NOT EXISTS geographic_coverage ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL +); + +-- Create table for sector coverage +CREATE TABLE IF NOT EXISTS sector_coverage ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) UNIQUE NOT NULL +); + +-- Create junction table for scenarios and geographic coverage +CREATE TABLE IF NOT EXISTS scenario_geographic_coverage ( + scenario_id INTEGER REFERENCES scenarios(id) ON DELETE CASCADE, + geographic_coverage_id INTEGER REFERENCES geographic_coverage(id) ON DELETE CASCADE, + PRIMARY KEY (scenario_id, geographic_coverage_id) +); + +-- Create junction table for scenarios and sector coverage +CREATE TABLE IF NOT EXISTS scenario_sector_coverage ( + scenario_id INTEGER REFERENCES scenarios(id) ON DELETE CASCADE, + sector_coverage_id INTEGER REFERENCES sector_coverage(id) ON DELETE CASCADE, + PRIMARY KEY (scenario_id, sector_coverage_id) +); + +-- Create a trigger function to update the "updated_on" column +CREATE OR REPLACE FUNCTION update_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_on = CURRENT_TIMESTAMP; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Add trigger to the "organizations" table +CREATE TRIGGER set_updated_on_organizations +BEFORE UPDATE ON organizations +FOR EACH ROW +EXECUTE FUNCTION update_timestamp(); + +-- Add trigger to the "scenarios" table +CREATE TRIGGER set_updated_on_scenarios +BEFORE UPDATE ON scenarios +FOR EACH ROW +EXECUTE FUNCTION update_timestamp(); + +-- Insert data into organizations +INSERT INTO organizations (name, logo_url) VALUES +('ASEAN Centre for Energy', 'https://www.aseanenergy.org/wp-content/themes/ace/assets/img/logo.png'), +('International Energy Agency', 'https://www.iea.org/assets/images/iea-logo.svg'), +('Asian Development Bank', 'https://www.adb.org/sites/default/files/styles/content_media/public/adb-logo-large.png'), +('Rocky Mountain Institute', 'https://rmi.org/wp-content/uploads/2018/09/rmi-logo-primary.svg'), +('ASEAN Energy Regulatory Network', 'https://asean.org/wp-content/uploads/2021/01/asean-logo.png'), +('World Bank Group', 'https://www.worldbank.org/content/dam/wbr/logo/logo-wb-header-en.svg'), +('National Renewable Energy Laboratory', 'https://www.nrel.gov/images/logo-nrel.svg'); + +-- Insert data into scenarios +INSERT INTO scenarios (name, description, usage, time_horizon, source, nature, target_temperature, organization_id) VALUES +('ASEAN Power Grid Integration', 'A scenario focused on regional power grid integration across ASEAN countries to enhance energy security, accessibility, and affordability while transitioning to cleaner energy sources.', 'Planning', '2035', 'ASEAN Centre for Energy', 'Normative', '2.0°C target', (SELECT id FROM organizations WHERE name = 'ASEAN Centre for Energy')), +('South-East Asia Energy Transition', 'A comprehensive pathway for South-East Asian utilities to transition from fossil fuels to renewable energy while maintaining grid reliability and economic growth.', 'Policy', '2050', 'International Energy Agency', 'Normative', '1.5°C - 2.0°C', (SELECT id FROM organizations WHERE name = 'International Energy Agency')), +('Utility Resilience in Rising Seas', 'A scenario addressing climate adaptation for coastal utility infrastructure in South-East Asia, focusing on resilience against sea level rise and extreme weather events.', 'Planning', '2050', 'Asian Development Bank', 'Descriptive', '2.5°C assumed', (SELECT id FROM organizations WHERE name = 'Asian Development Bank')), +('Electrification & Decarbonization Pathway', 'A detailed roadmap for utilities in South-East Asia to support economy-wide electrification while decarbonizing the power sector through renewable integration.', 'Planning', '2050', 'Rocky Mountain Institute', 'Normative', '1.5°C target', (SELECT id FROM organizations WHERE name = 'Rocky Mountain Institute')), +('ASEAN Interconnection Masterplan', 'A strategic plan for developing cross-border power interconnections to create an integrated ASEAN power grid, enabling renewable energy sharing across the region.', 'Policy', '2040', 'ASEAN Energy Regulatory Network', 'Normative', '2.0°C target', (SELECT id FROM organizations WHERE name = 'ASEAN Energy Regulatory Network')), +('Climate-Resilient Utility Infrastructure', 'A scenario focused on climate-proofing critical utility infrastructure in South-East Asia against increasing extreme weather events and changing climate patterns.', 'Planning', '2040', 'World Bank Group', 'Descriptive', '3.0°C projected', (SELECT id FROM organizations WHERE name = 'World Bank Group')), +('Distributed Energy Resources Integration', 'A scenario exploring the integration of distributed energy resources into South-East Asian utility grids, including rooftop solar, battery storage, and microgrids.', 'Research', '2035', 'National Renewable Energy Laboratory', 'Descriptive', '2.0°C - 2.5°C', (SELECT id FROM organizations WHERE name = 'National Renewable Energy Laboratory')), +('South-East Asia Net-Zero Utilities', 'A comprehensive pathway for utility companies in South-East Asia to achieve net-zero emissions while supporting economic development and energy access.', 'Policy', '2050', 'Rocky Mountain Institute', 'Normative', '1.5°C target', (SELECT id FROM organizations WHERE name = 'Rocky Mountain Institute')); + +-- Insert data into geographic_coverage +INSERT INTO geographic_coverage (name) VALUES +('South-East Asia'), +('Thailand'), +('Malaysia'), +('Indonesia'), +('Vietnam'), +('Philippines'), +('Singapore'), +('Laos'), +('Cambodia'), +('Myanmar'); + +-- Insert data into sector_coverage +INSERT INTO sector_coverage (name) VALUES +('Utilities'), +('Energy'), +('Renewable Energy'), +('Grid Infrastructure'), +('Policy'), +('Coastal Infrastructure'), +('Water Management'), +('Disaster Response'), +('Transportation'), +('Energy Storage'), +('Smart Grid'), +('Carbon Capture'), +('Energy Efficiency'), +('Urban Infrastructure'); + +-- Insert data into scenario_geographic_coverage +INSERT INTO scenario_geographic_coverage (scenario_id, geographic_coverage_id) +SELECT s.id, g.id FROM scenarios s, geographic_coverage g +WHERE s.name = 'ASEAN Power Grid Integration' AND g.name IN ('South-East Asia', 'Thailand', 'Malaysia', 'Indonesia', 'Vietnam', 'Philippines'); + +INSERT INTO scenario_geographic_coverage (scenario_id, geographic_coverage_id) +SELECT s.id, g.id FROM scenarios s, geographic_coverage g +WHERE s.name = 'South-East Asia Energy Transition' AND g.name IN ('South-East Asia', 'Singapore', 'Malaysia', 'Indonesia', 'Vietnam'); + +-- Insert data into scenario_sector_coverage +INSERT INTO scenario_sector_coverage (scenario_id, sector_coverage_id) +SELECT s.id, sc.id FROM scenarios s, sector_coverage sc +WHERE s.name = 'ASEAN Power Grid Integration' AND sc.name IN ('Utilities', 'Energy', 'Renewable Energy', 'Grid Infrastructure'); + +INSERT INTO scenario_sector_coverage (scenario_id, sector_coverage_id) +SELECT s.id, sc.id FROM scenarios s, sector_coverage sc +WHERE s.name = 'South-East Asia Energy Transition' AND sc.name IN ('Utilities', 'Energy', 'Policy', 'Infrastructure'); + +--Add Search Capability to Organizations table + +-- Add a tsvector column for full-text search +ALTER TABLE organizations ADD COLUMN IF NOT EXISTS search_vector tsvector; + +-- Populate the search_vector column with data from the name field +UPDATE organizations +SET search_vector = to_tsvector('english', coalesce(name, '')); + +-- Create a GIN index on the search_vector column for efficient full-text search +CREATE INDEX IF NOT EXISTS search_vector_idx +ON organizations USING gin(search_vector); + +-- Create a trigger to automatically update the search_vector column on insert or update +CREATE OR REPLACE FUNCTION update_search_vector() +RETURNS trigger AS $$ +BEGIN + NEW.search_vector := to_tsvector('english', coalesce(NEW.name, '')); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER update_search_vector_trigger +BEFORE INSERT OR UPDATE ON organizations +FOR EACH ROW EXECUTE FUNCTION update_search_vector(); diff --git a/database/test/delete.sql b/database/test/delete.sql new file mode 100644 index 0000000..243377b --- /dev/null +++ b/database/test/delete.sql @@ -0,0 +1,14 @@ +-- Delete a scenario and ensure cascading deletes +DELETE FROM poc.scenarios +WHERE name = 'Test Scenario'; + +-- Verify cascading deletes (should return no rows) +SELECT * FROM poc.scenario_geographic_coverage +WHERE scenario_id = (SELECT id FROM poc.scenarios WHERE name = 'Test Scenario'); + +SELECT * FROM poc.scenario_sector_coverage +WHERE scenario_id = (SELECT id FROM poc.scenarios WHERE name = 'Test Scenario'); + +-- Delete an organization (should fail if referenced by a scenario) +DELETE FROM poc.organizations +WHERE name = 'Updated Test Organization'; diff --git a/database/test/read.sql b/database/test/read.sql new file mode 100644 index 0000000..ce0af61 --- /dev/null +++ b/database/test/read.sql @@ -0,0 +1,20 @@ +-- Retrieve all organizations +SELECT * FROM poc.organizations; + +-- Retrieve all scenarios +SELECT * FROM poc.scenarios; + +-- Retrieve a specific scenario by name +SELECT * FROM poc.scenarios WHERE name = 'Test Scenario'; + +-- Retrieve all geographic coverage for a specific scenario +SELECT g.name +FROM poc.scenario_geographic_coverage sgc +JOIN poc.geographic_coverage g ON sgc.geographic_coverage_id = g.id +WHERE sgc.scenario_id = (SELECT id FROM poc.scenarios WHERE name = 'Test Scenario'); + +-- Retrieve all sector coverage for a specific scenario +SELECT sc.name +FROM poc.scenario_sector_coverage ssc +JOIN poc.sector_coverage sc ON ssc.sector_coverage_id = sc.id +WHERE ssc.scenario_id = (SELECT id FROM poc.scenarios WHERE name = 'Test Scenario'); diff --git a/database/test/test_scenario.sql b/database/test/test_scenario.sql new file mode 100644 index 0000000..4589eb9 --- /dev/null +++ b/database/test/test_scenario.sql @@ -0,0 +1,42 @@ +-- Use poc as seach path +-- This is still needed for queries in PG ADMIN, despite being set on init +SET search_path TO poc; + +-- Insert a new organization +INSERT INTO organizations (name, logo_url) +VALUES ('Test Organization', 'https://example.com/logo.png'); + +-- Insert a new scenario +INSERT INTO scenarios (name, description, usage, time_horizon, source, nature, target_temperature, organization_id) +VALUES ( + 'Test Scenario', + 'A test scenario for validation.', + 'Planning', + '2030', + 'Test Source', + 'Normative', + '1.5°C target', + (SELECT id FROM organizations WHERE name = 'Test Organization') +); + +-- Insert a new geographic coverage +INSERT INTO geographic_coverage (name) +VALUES ('Test Region'); + +-- Insert a new sector coverage +INSERT INTO sector_coverage (name) +VALUES ('Test Sector'); + +-- Link the scenario to geographic coverage +INSERT INTO scenario_geographic_coverage (scenario_id, geographic_coverage_id) +VALUES ( + (SELECT id FROM scenarios WHERE name = 'Test Scenario'), + (SELECT id FROM geographic_coverage WHERE name = 'Test Region') +); + +-- Link the scenario to sector coverage +INSERT INTO scenario_sector_coverage (scenario_id, sector_coverage_id) +VALUES ( + (SELECT id FROM scenarios WHERE name = 'Test Scenario'), + (SELECT id FROM sector_coverage WHERE name = 'Test Sector') +); diff --git a/database/test/update.sql b/database/test/update.sql new file mode 100644 index 0000000..60d6a62 --- /dev/null +++ b/database/test/update.sql @@ -0,0 +1,17 @@ +SET search_path TO poc; + +-- Update the name of an organization +UPDATE organizations +SET name = 'Updated Test Organization' +WHERE name = 'Test Organization'; + +-- Verify the "updated_on" column +SELECT * FROM organizations WHERE name = 'Updated Test Organization'; + +-- Update the description of a scenario +UPDATE scenarios +SET description = 'An updated test scenario description.' +WHERE name = 'Test Scenario'; + +-- Verify the "updated_on" column +SELECT * FROM scenarios WHERE name = 'Test Scenario'; diff --git a/docker-compose.yaml b/docker-compose.yaml deleted file mode 100644 index 5be68e1..0000000 --- a/docker-compose.yaml +++ /dev/null @@ -1,8 +0,0 @@ - -services: - api: - build: - context: . - container_name: web-api-poc-app - ports: - - '80:5000' diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..47ab019 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ + +services: + api: + build: + context: api + environment: + - POC_API_KEY=${POC_API_KEY:-abc123} + - POC_API_PORT=${POC_API_PORT:-8000} + - POC_API_LOG_LEVEL=${POC_API_LOG_LEVEL:-info} + - POC_DB_PORT=${POC_DB_PORT:-5432} + - PYTHONUNBUFFERED=1 + ports: + - "${POC_API_PORT:-8000}:${POC_API_PORT:-8000}" + db: + image: postgres:16 + restart: always + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: poc + POC_DB_PORT: ${POC_DB_PORT:-5432} + ports: + - "${POC_DB_PORT:-5432}:${POC_DB_PORT:-5432}" + command: -p ${POC_DB_PORT:-5432} + volumes: + - ./database/init:/docker-entrypoint-initdb.d + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + +volumes: + postgres_data: + driver: local diff --git a/example.env b/example.env new file mode 100644 index 0000000..05ffc11 --- /dev/null +++ b/example.env @@ -0,0 +1,5 @@ +POC_API_LOG_LEVEL=info +POC_API_KEY=abc123 +POC_API_PORT=8000 +POC_DB_PORT=5432 +POC_FRONTEND_PORT=80 diff --git a/main.py b/main.py deleted file mode 100644 index 881b83b..0000000 --- a/main.py +++ /dev/null @@ -1,46 +0,0 @@ -from fastapi import FastAPI -from fastapi.responses import RedirectResponse -from src.routers.health import health_router -from src.routers.mtcars import data_output -import uvicorn -import tomllib - -# Import pyproject toml info using tomllib -try: - with open("pyproject.toml", "rb") as f: - tomldata = tomllib.load(f) - version = tomldata["project"]["version"] - description = tomldata["project"]["description"] -except FileNotFoundError: - print("pyproject.toml not found") - -app = FastAPI( - # This info goes directly into /docs - title="RMI Web API poc", - # Description of API defined in docs/documentation.py for ease of reading - description=description, - summary="This project is a proof-of-concept (POC) web API built using the FastAPI library.", - version="0.0.1", - contact={ - "name": "RMI", - "url": "https://github.com/RMI", - }, - license_info={ - "name": "MIT", - "url": "https://github.com/RMI/web-api-poc/blob/main/LICENSE.txt", - }, -) - - -@app.get("/") -async def redirect(): - response = RedirectResponse(url="/docs") - return response - - -app.include_router(health_router) -app.include_router(data_output) - - -if __name__ == "__main__": - uvicorn.run("main:app", host="0.0.0.0", port=5000, log_level="info") diff --git a/src/data/mtcars.csv b/src/data/mtcars.csv deleted file mode 100644 index c2c6256..0000000 --- a/src/data/mtcars.csv +++ /dev/null @@ -1,33 +0,0 @@ -model,mpg,cyl,disp,hp,drat,wt,qsec,vs,am,gear,carb -Mazda RX4,21,6,160,110,3.9,2.62,16.46,0,1,4,4 -Mazda RX4 Wag,21,6,160,110,3.9,2.875,17.02,0,1,4,4 -Datsun 710,22.8,4,108,93,3.85,2.32,18.61,1,1,4,1 -Hornet 4 Drive,21.4,6,258,110,3.08,3.215,19.44,1,0,3,1 -Hornet Sportabout,18.7,8,360,175,3.15,3.44,17.02,0,0,3,2 -Valiant,18.1,6,225,105,2.76,3.46,20.22,1,0,3,1 -Duster 360,14.3,8,360,245,3.21,3.57,15.84,0,0,3,4 -Merc 240D,24.4,4,146.7,62,3.69,3.19,20,1,0,4,2 -Merc 230,22.8,4,140.8,95,3.92,3.15,22.9,1,0,4,2 -Merc 280,19.2,6,167.6,123,3.92,3.44,18.3,1,0,4,4 -Merc 280C,17.8,6,167.6,123,3.92,3.44,18.9,1,0,4,4 -Merc 450SE,16.4,8,275.8,180,3.07,4.07,17.4,0,0,3,3 -Merc 450SL,17.3,8,275.8,180,3.07,3.73,17.6,0,0,3,3 -Merc 450SLC,15.2,8,275.8,180,3.07,3.78,18,0,0,3,3 -Cadillac Fleetwood,10.4,8,472,205,2.93,5.25,17.98,0,0,3,4 -Lincoln Continental,10.4,8,460,215,3,5.424,17.82,0,0,3,4 -Chrysler Imperial,14.7,8,440,230,3.23,5.345,17.42,0,0,3,4 -Fiat 128,32.4,4,78.7,66,4.08,2.2,19.47,1,1,4,1 -Honda Civic,30.4,4,75.7,52,4.93,1.615,18.52,1,1,4,2 -Toyota Corolla,33.9,4,71.1,65,4.22,1.835,19.9,1,1,4,1 -Toyota Corona,21.5,4,120.1,97,3.7,2.465,20.01,1,0,3,1 -Dodge Challenger,15.5,8,318,150,2.76,3.52,16.87,0,0,3,2 -AMC Javelin,15.2,8,304,150,3.15,3.435,17.3,0,0,3,2 -Camaro Z28,13.3,8,350,245,3.73,3.84,15.41,0,0,3,4 -Pontiac Firebird,19.2,8,400,175,3.08,3.845,17.05,0,0,3,2 -Fiat X1-9,27.3,4,79,66,4.08,1.935,18.9,1,1,4,1 -Porsche 914-2,26,4,120.3,91,4.43,2.14,16.7,0,1,5,2 -Lotus Europa,30.4,4,95.1,113,3.77,1.513,16.9,1,1,5,2 -Ford Pantera L,15.8,8,351,264,4.22,3.17,14.5,0,1,5,4 -Ferrari Dino,19.7,6,145,175,3.62,2.77,15.5,0,1,5,6 -Maserati Bora,15,8,301,335,3.54,3.57,14.6,0,1,5,8 -Volvo 142E,21.4,4,121,109,4.11,2.78,18.6,1,1,4,2 diff --git a/src/docs/developer_journal.md b/src/docs/developer_journal.md deleted file mode 100644 index 0aa9b23..0000000 --- a/src/docs/developer_journal.md +++ /dev/null @@ -1,14 +0,0 @@ -# Developer Journal - -## 2025-02-18 SAM -Set up initial folder structure and root endpoint for API that returns "Hello World". Used UV for environment management. First router is a "health" endpoint that returns http status "OK", used health router to set up models and routers folders, defining the health model and router respectively. Mostly smooth sailing so far. Watched Youtube tutorials for UV and FastAPI, both have been intuitive and well-documented. - -## 2025-02-21 SAM -Built data output routers for mtcars dataset. {root}/api/dataset returns full mtcars dataset in json. {root}/api/{item_id} returns individual mtcars item, with the item_id as the model name with spaces removed. Static data currently held in src/data. src/services/mtcars.py ingests dataset and returns a dictionary with the full dataset. HTTP responses encoded into routers, with 404 Item Not Found for {item_id} outside of the mtcars set (using HondaAccord to test). Status 200 OK returns for "good" requests. Status 500 Internal Server Error seems to be automatic in FastAPI, will return if code is not working (trying to dump entire mtcars dataset as 'str' instead of as json encoded). PR of mtcars work did create a small merge conflict with main branch, but should be fairly easy to resolve. -Set up initial folder structure and root endpoint for API that returns "Hello World". Used UV for environment management. First router is a "health" endpoint that returns http status "OK", used health router to set up models and routers folders, defining the health model and router respectively. Mostly smooth sailing so far. Watched Youtube tutorials for UV and FastAPI, both have been intuitive and well-documented. - -## 2025-02-26 SAM -Discovered issues when implementing unit and integration tests. Single instance of mtcars would pass the integration test, but was unable to get the full dataset to pass a similar test. Tried to resolve by making a new model as a list of mtcar instances, but this did not work. Eventually while trying to figure out what was going on, reworked the services function to validate each row in the csv against the mtcars model. It now creates a list of validated mtcars, which is stored as MTCARS_DATA. In order to pass validation for the full dataset, a new test was created which checks to make sure the full dataset is a list, then checks each individual item in the list against the mtcar model. Some updates to the router logic as well to handle the new data structures. - -## 2025-03-10 -Dockerized API this past week. Runs through Dockerfile and a docker-compose.yaml. Fairly simple setup, good documentation out there. One quirk is the networking. 5000 port was in use, had to change that on the local network size. IP and Port shown in the command line is not where the app is currently located locally (at localhost), but this is likely to not matter as we move the app onto Azure and modify the way logging is handled. Next steps are to move onto Azure and create API key authentification. diff --git a/src/models/outputs.py b/src/models/outputs.py deleted file mode 100644 index c9ae1eb..0000000 --- a/src/models/outputs.py +++ /dev/null @@ -1,17 +0,0 @@ -from pydantic import BaseModel - - -# create Pydantic model for mtcars dataset -class mtcar(BaseModel): - model: str - mpg: float - cyl: int - disp: float - hp: int - drat: float - wt: float - qsec: float - vs: bool - am: bool - gear: int - carb: int diff --git a/src/routers/__init__.py b/src/routers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/routers/mtcars.py b/src/routers/mtcars.py deleted file mode 100644 index 61c9439..0000000 --- a/src/routers/mtcars.py +++ /dev/null @@ -1,38 +0,0 @@ -from fastapi import APIRouter, status, HTTPException, Depends -from fastapi.responses import JSONResponse -from models.outputs import mtcar -from services.mtcars import MTCARS_DATA -from services.auth import get_api_key - -data_output = APIRouter() - - -@data_output.get( - # api endpoint for complete mtcars dataset - "/api/dataset", - tags=["data"], - summary="Return mtcars dataset ", - response_description="mtcars dataset in json", - status_code=status.HTTP_200_OK, - response_model=list[mtcar], -) -async def get_full_mtcars(api_key: str = Depends(get_api_key)): - return [mtcar(**b) for b in MTCARS_DATA] - - -@data_output.get( - # api endpoint for individual mtcars items - "/api/{model_name}", - tags=["data"], - summary="Return mtcars item", - response_description="mtcars item in json", - status_code=status.HTTP_200_OK, - response_model=mtcar, -) -async def read_item(model_name: str, api_key: str = Depends(get_api_key)): - # searches MTCARS_DATA for specific model name - for item in MTCARS_DATA: - if item["model"] == model_name: - return item - # 404 Item not found for when model name does not exist in MTCARS_DATA - raise HTTPException(status_code=404, detail="Item not found") diff --git a/src/services/__init__.py b/src/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/services/mtcars.py b/src/services/mtcars.py deleted file mode 100644 index 576b314..0000000 --- a/src/services/mtcars.py +++ /dev/null @@ -1,28 +0,0 @@ -import csv -from pydantic import ValidationError -from importlib import resources -from models.outputs import mtcar - - -# Function to read in csv, validate using mtcar model and output mtcars data in JSON -def mtcars_csv_to_dict(mtcars_path): - mtcars_data = [] - with open(mtcars_path) as f: - reader = csv.DictReader(f) - for row in reader: - try: - # replaces the space in the model of each row, so it will be a valid url later - row["model"] = row["model"].replace(" ", "") - # validates row of csv against mtcar model - validated_row = mtcar.model_validate(row) - # dumps individual validated "mtcar" into list mtcars_data - mtcars_data.append(validated_row.model_dump()) - # error message for if a row fails validation - except ValidationError as e: - print(f"Error validating row {row}: {e}") - return mtcars_data - - -mtcars_path = resources.files("data").joinpath("mtcars.csv") - -MTCARS_DATA = mtcars_csv_to_dict(mtcars_path) diff --git a/tests/test-integration.sh b/tests/test-integration.sh new file mode 100755 index 0000000..c9ea0c3 --- /dev/null +++ b/tests/test-integration.sh @@ -0,0 +1,93 @@ +#!/bin/bash + +check_response_code() +{ + URL=$1 + EXPECT=$2 + RESPONSE_CODE=$( \ + curl \ + --silent \ + --output /dev/null \ + --write-out "%{http_code}" \ + "$URL" + ) + + if [ "$RESPONSE_CODE" != "$EXPECT" ] + then + echo ❌ "$URL": "$RESPONSE_CODE" \(expected: "$EXPECT"\) + exit 1 + else + echo ✅ "$URL": "$RESPONSE_CODE" \(expected: "$EXPECT"\) + fi +} + +echo -e "\n\ntesting response code:" +check_response_code "http://localhost:8000/" 307 +check_response_code "http://localhost:8000/docs" 200 +check_response_code "http://localhost:8000/docs/" 307 +check_response_code "http://localhost:8000/health" 200 +check_response_code "http://localhost:8000/health/" 307 +check_response_code "http://localhost:8000/tables" 200 +check_response_code "http://localhost:8000/tables/" 307 +check_response_code "http://localhost:8000/scenarios" 200 +check_response_code "http://localhost:8000/scenarios/" 307 +check_response_code "http://localhost:8000/scenarios/1" 200 +check_response_code "http://localhost:8000/scenarios/1/" 307 +check_response_code "http://localhost:8000/organizations" 200 +check_response_code "http://localhost:8000/organizations/" 307 +check_response_code "http://localhost:8000/organizations/1" 200 +check_response_code "http://localhost:8000/organizations/1/" 307 +check_response_code "http://localhost:8000/xxx" 404 + + +check_response() +{ + URL=$1 + EXPECT_JSON=$2 + RESPONSE=$(curl --silent "$URL") + + # Extract keys from expected JSON + EXPECTED_KEYS=$(echo "$EXPECT_JSON" | jq -r 'keys[]') + + # Check each expected key exists with correct value + for KEY in $EXPECTED_KEYS; do + EXPECTED_VALUE=$(echo "$EXPECT_JSON" | jq -r ".[\"$KEY\"]") + ACTUAL_VALUE=$(echo "$RESPONSE" | jq -r ".[\"$KEY\"]" 2>/dev/null) + + if [ "$ACTUAL_VALUE" == "$EXPECTED_VALUE" ]; then + echo "✅ $URL: Key '$KEY' has expected value '$EXPECTED_VALUE'" + else + echo "❌ $URL: Key '$KEY' does not match expected value" + echo "Expected: $EXPECTED_VALUE" + echo "Actual: $ACTUAL_VALUE" + exit 1 + fi + done +} + +echo -e "\n\nchecking response key-value pairs:" +check_response "http://localhost:8000/foo" '{"detail":"Not Found"}' +check_response "http://localhost:8000/health" '{"status":"OK"}' +check_response "http://localhost:8000/scenarios/1" '{"id":"1"}' +check_response "http://localhost:8000/organizations/1" '{"id":"1"}' + +# Check that an array contains an element with specific key-value pair +check_array_contains() { + URL=$1 + KEY=$2 + VALUE=$3 + RESPONSE=$(curl --silent "$URL") + + if echo "$RESPONSE" | jq -e "map(select(.$KEY == \"$VALUE\")) | length > 0" > /dev/null; then + echo "✅ $URL: Found array element with $KEY = '$VALUE'" + else + echo "❌ $URL: No array element with $KEY = '$VALUE' found" + echo "Expected to find element with: $KEY = $VALUE" + echo "Response first few elements: $(echo "$RESPONSE" | jq '.[0:2]')" + exit 1 + fi +} + +echo -e "\n\nchecking that arrays contain an element with key-value pair:" +check_array_contains "http://localhost:8000/scenarios" "time_horizon" "2035" +check_array_contains "http://localhost:8000/organizations" "name" "World Bank Group" diff --git a/tests/test_integration.py b/tests/test_integration.py deleted file mode 100644 index 0429b94..0000000 --- a/tests/test_integration.py +++ /dev/null @@ -1,48 +0,0 @@ -from fastapi.testclient import TestClient -from main import app -from models.outputs import mtcar -from services.auth import get_api_key - - -def override_get_api_key(): - return True - - -app.dependency_overrides[get_api_key] = override_get_api_key - -client = TestClient(app) - - -def test_health_check(): - response = client.get("/health/") - assert response.status_code == 200 - assert response.json() == {"status": "OK"} - - -def test_element_model(): - response = client.get("/api/HondaCivic") - assert response.status_code == 200 - # Validate single mtcars instance against mtcar model - mtcar.model_validate(response.json()) - - -def test_dataset_model(): - response = client.get("/api/dataset") - assert response.status_code == 200 - # Validate that full dataset is a list and each list item is mtcar - mtcars = response.json() - assert isinstance(mtcars, list) - for mtcar_data in mtcars: - mtcar(**mtcar_data) - - -def test_root_redirects(): - response = client.get("/", follow_redirects=False) - assert response.status_code == 307 # Temporary Redirect - assert response.headers["location"] == "/docs" - - -def test_root_redirect_follows(): - response = client.get("/", follow_redirects=True) - assert response.status_code == 200 - assert "Swagger UI" in response.text diff --git a/uv.lock b/uv.lock deleted file mode 100644 index e2b87e6..0000000 --- a/uv.lock +++ /dev/null @@ -1,425 +0,0 @@ -version = 1 -requires-python = ">=3.12" - -[[package]] -name = "annotated-types" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, -] - -[[package]] -name = "anyio" -version = "4.8.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "idna" }, - { name = "sniffio" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, -] - -[[package]] -name = "black" -version = "25.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/94/49/26a7b0f3f35da4b5a65f081943b7bcd22d7002f5f0fb8098ec1ff21cb6ef/black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666", size = 649449 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/83/71/3fe4741df7adf015ad8dfa082dd36c94ca86bb21f25608eb247b4afb15b2/black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b", size = 1650988 }, - { url = "https://files.pythonhosted.org/packages/13/f3/89aac8a83d73937ccd39bbe8fc6ac8860c11cfa0af5b1c96d081facac844/black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc", size = 1453985 }, - { url = "https://files.pythonhosted.org/packages/6f/22/b99efca33f1f3a1d2552c714b1e1b5ae92efac6c43e790ad539a163d1754/black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f", size = 1783816 }, - { url = "https://files.pythonhosted.org/packages/18/7e/a27c3ad3822b6f2e0e00d63d58ff6299a99a5b3aee69fa77cd4b0076b261/black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba", size = 1440860 }, - { url = "https://files.pythonhosted.org/packages/98/87/0edf98916640efa5d0696e1abb0a8357b52e69e82322628f25bf14d263d1/black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f", size = 1650673 }, - { url = "https://files.pythonhosted.org/packages/52/e5/f7bf17207cf87fa6e9b676576749c6b6ed0d70f179a3d812c997870291c3/black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3", size = 1453190 }, - { url = "https://files.pythonhosted.org/packages/e3/ee/adda3d46d4a9120772fae6de454c8495603c37c4c3b9c60f25b1ab6401fe/black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171", size = 1782926 }, - { url = "https://files.pythonhosted.org/packages/cc/64/94eb5f45dcb997d2082f097a3944cfc7fe87e071907f677e80788a2d7b7a/black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18", size = 1442613 }, - { url = "https://files.pythonhosted.org/packages/09/71/54e999902aed72baf26bca0d50781b01838251a462612966e9fc4891eadd/black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717", size = 207646 }, -] - -[[package]] -name = "certifi" -version = "2025.1.31" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, -] - -[[package]] -name = "click" -version = "8.1.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, -] - -[[package]] -name = "coverage" -version = "7.6.12" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, - { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, - { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, - { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, - { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, - { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, - { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, - { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, - { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, - { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, - { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, - { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, - { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, - { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, - { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, - { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, - { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, - { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, - { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, - { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, - { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, - { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, - { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, - { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, - { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, - { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, - { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, - { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, - { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, - { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, - { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, -] - -[[package]] -name = "dotenv" -version = "0.9.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dotenv" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892 }, -] - -[[package]] -name = "fastapi" -version = "0.115.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydantic" }, - { name = "starlette" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a2/b2/5a5dc4affdb6661dea100324e19a7721d5dc524b464fe8e366c093fd7d87/fastapi-0.115.8.tar.gz", hash = "sha256:0ce9111231720190473e222cdf0f07f7206ad7e53ea02beb1d2dc36e2f0741e9", size = 295403 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8f/7d/2d6ce181d7a5f51dedb8c06206cbf0ec026a99bf145edd309f9e17c3282f/fastapi-0.115.8-py3-none-any.whl", hash = "sha256:753a96dd7e036b34eeef8babdfcfe3f28ff79648f86551eb36bfc1b0bf4a8cbf", size = 94814 }, -] - -[[package]] -name = "h11" -version = "0.14.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, -] - -[[package]] -name = "httpcore" -version = "1.0.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, -] - -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, -] - -[[package]] -name = "idna" -version = "3.10" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, -] - -[[package]] -name = "iniconfig" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, -] - -[[package]] -name = "packaging" -version = "24.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, -] - -[[package]] -name = "pathspec" -version = "0.12.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191 }, -] - -[[package]] -name = "platformdirs" -version = "4.3.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, -] - -[[package]] -name = "pluggy" -version = "1.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, -] - -[[package]] -name = "pydantic" -version = "2.10.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "annotated-types" }, - { name = "pydantic-core" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, -] - -[[package]] -name = "pydantic-core" -version = "2.27.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, - { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, - { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, - { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, - { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, - { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, - { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, - { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, - { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, - { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, - { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, - { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, - { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, - { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, -] - -[[package]] -name = "pytest" -version = "8.3.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, -] - -[[package]] -name = "pytest-asyncio" -version = "0.25.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f2/a8/ecbc8ede70921dd2f544ab1cadd3ff3bf842af27f87bbdea774c7baa1d38/pytest_asyncio-0.25.3.tar.gz", hash = "sha256:fc1da2cf9f125ada7e710b4ddad05518d4cee187ae9412e9ac9271003497f07a", size = 54239 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/67/17/3493c5624e48fd97156ebaec380dcaafee9506d7e2c46218ceebbb57d7de/pytest_asyncio-0.25.3-py3-none-any.whl", hash = "sha256:9e89518e0f9bd08928f97a3482fdc4e244df17529460bc038291ccaf8f85c7c3", size = 19467 }, -] - -[[package]] -name = "pytest-cov" -version = "6.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, -] - -[[package]] -name = "python-dotenv" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, -] - -[[package]] -name = "starlette" -version = "0.45.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ff/fb/2984a686808b89a6781526129a4b51266f678b2d2b97ab2d325e56116df8/starlette-0.45.3.tar.gz", hash = "sha256:2cbcba2a75806f8a41c722141486f37c28e30a0921c5f6fe4346cb0dcee1302f", size = 2574076 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/61/f2b52e107b1fc8944b33ef56bf6ac4ebbe16d91b94d2b87ce013bf63fb84/starlette-0.45.3-py3-none-any.whl", hash = "sha256:dfb6d332576f136ec740296c7e8bb8c8a7125044e7c6da30744718880cdd059d", size = 71507 }, -] - -[[package]] -name = "typing-extensions" -version = "4.12.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, -] - -[[package]] -name = "uvicorn" -version = "0.34.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, -] - -[[package]] -name = "web-api-poc" -version = "0.1.0" -source = { editable = "." } -dependencies = [ - { name = "dotenv" }, - { name = "fastapi" }, - { name = "pydantic" }, - { name = "uvicorn" }, -] - -[package.dev-dependencies] -dev = [ - { name = "black" }, - { name = "httpx" }, - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-cov" }, -] - -[package.metadata] -requires-dist = [ - { name = "dotenv", specifier = ">=0.9.9" }, - { name = "fastapi", specifier = ">=0.115.8" }, - { name = "pydantic", specifier = ">=2.10.6" }, - { name = "uvicorn", specifier = ">=0.34.0" }, -] - -[package.metadata.requires-dev] -dev = [ - { name = "black", specifier = ">=25.1.0" }, - { name = "httpx", specifier = ">=0.28.1" }, - { name = "pytest", specifier = ">=8.3.4" }, - { name = "pytest-asyncio", specifier = ">=0.25.3" }, - { name = "pytest-cov", specifier = ">=6.0.0" }, -]