diff --git a/.envs/dev/backend.env.example b/.envs/dev/backend.env.example index e1d2f6b..43269ec 100644 --- a/.envs/dev/backend.env.example +++ b/.envs/dev/backend.env.example @@ -51,7 +51,7 @@ GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret # Must match the callback URL in your GitHub App configuration # This is the backend OAuth callback endpoint that GitHub calls with the authorization code. -GITHUB_REDIRECT_URL=https://127.0.0.1/auth/github/callback +GITHUB_REDIRECT_URL=https://127.0.0.1/api/v1/auth/github/callback # Secret used to sign OAuth "state" parameter (prevents CSRF) # Generate securely with: diff --git a/.envs/dev/frontend.env.example b/.envs/dev/frontend.env.example index ee1bb99..c2c1d87 100644 --- a/.envs/dev/frontend.env.example +++ b/.envs/dev/frontend.env.example @@ -4,4 +4,4 @@ # Backend configuration # ------------------------------------------------------------------- -VITE_API_BASE_URL=https://127.0.0.1:8000/api +VITE_API_BASE_URL=https://127.0.0.1:8000/api/v1 diff --git a/.envs/dev_docker/backend.env.example b/.envs/dev_docker/backend.env.example index 0b6977f..b7ebb0b 100644 --- a/.envs/dev_docker/backend.env.example +++ b/.envs/dev_docker/backend.env.example @@ -51,7 +51,7 @@ GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret # Must match the callback URL in your GitHub App configuration # This is the backend OAuth callback endpoint that GitHub calls with the authorization code. -GITHUB_REDIRECT_URL=https://127.0.0.1/auth/github/callback +GITHUB_REDIRECT_URL=https://127.0.0.1/api/v1/auth/github/callback # Secret used to sign OAuth "state" parameter (prevents CSRF) # Generate securely with: diff --git a/.envs/dev_docker/frontend.env.example b/.envs/dev_docker/frontend.env.example index ee1bb99..c2c1d87 100644 --- a/.envs/dev_docker/frontend.env.example +++ b/.envs/dev_docker/frontend.env.example @@ -4,4 +4,4 @@ # Backend configuration # ------------------------------------------------------------------- -VITE_API_BASE_URL=https://127.0.0.1:8000/api +VITE_API_BASE_URL=https://127.0.0.1:8000/api/v1 diff --git a/.envs/prod/backend.env.example b/.envs/prod/backend.env.example index 4f1377d..bcc4baa 100644 --- a/.envs/prod/backend.env.example +++ b/.envs/prod/backend.env.example @@ -51,9 +51,9 @@ GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret # Must match the callback URL in your GitHub App configuration # This is the backend OAuth callback endpoint that GitHub calls with the authorization code. -GITHUB_REDIRECT_URL=https://127.0.0.1/auth/github/callback +GITHUB_REDIRECT_URL=https://127.0.0.1/api/v1/auth/github/callback # For production, this must also point to the backend's OAuth callback endpoint -# GITHUB_REDIRECT_URL=https://app.${DOMAIN}/auth/callback +# GITHUB_REDIRECT_URL=https://app.${DOMAIN}/api/v1/auth/callback # Secret used to sign OAuth "state" parameter (prevents CSRF) # Generate securely with: diff --git a/.envs/prod/frontend.env.example b/.envs/prod/frontend.env.example index ee1bb99..c2c1d87 100644 --- a/.envs/prod/frontend.env.example +++ b/.envs/prod/frontend.env.example @@ -4,4 +4,4 @@ # Backend configuration # ------------------------------------------------------------------- -VITE_API_BASE_URL=https://127.0.0.1:8000/api +VITE_API_BASE_URL=https://127.0.0.1:8000/api/v1 diff --git a/.github/workflows/build.yml b/.github/workflows/backend-ci.yml similarity index 80% rename from .github/workflows/build.yml rename to .github/workflows/backend-ci.yml index 28978ca..9384378 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/backend-ci.yml @@ -1,16 +1,25 @@ -name: CI +name: Backend CI on: pull_request: branches: [main] + paths: + - backend/** + - .pre-commit-config.yaml + - .github/workflows/backend-ci.yml push: branches: [main] + paths: + - backend/** + - .pre-commit-config.yaml + - .github/workflows/backend-ci.yml workflow_dispatch: {} jobs: backend: name: Backend checks runs-on: ubuntu-latest + timeout-minutes: 15 services: postgres: @@ -60,7 +69,7 @@ jobs: # -------------------------------------------------- GITHUB_CLIENT_ID: dummy GITHUB_CLIENT_SECRET: dummy - GITHUB_REDIRECT_URL: http://localhost/api/auth/github/callback + GITHUB_REDIRECT_URL: http://localhost/api/v1/auth/github/callback GITHUB_STATE_SECRET_KEY: dummy # -------------------------------------------------- @@ -108,15 +117,13 @@ jobs: # ------------------------- # Cache uv + dependencies # ------------------------- - - name: Cache uv + - name: Cache uv cache uses: actions/cache@v4 with: - path: | - ~/.cache/uv - backend/.venv - key: uv-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }} + path: ~/.cache/uv + key: uv-cache-${{ runner.os }}-${{ hashFiles('backend/uv.lock') }} restore-keys: | - uv-${{ runner.os }}- + uv-cache-${{ runner.os }}- # ------------------------- # Install backend deps @@ -135,21 +142,24 @@ jobs: with: path: ~/.cache/pre-commit key: pre-commit-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - pre-commit-${{ runner.os }}- # ------------------------- # Run pre-commit # ------------------------- - - name: Run pre-commit - working-directory: backend + - name: Run pre-commit (backend only) + env: + SKIP: frontend-eslint,frontend-prettier run: | - uv run pre-commit run --all-files + uv run --project backend pre-commit run --all-files # ------------------------- # Run pytest # ------------------------- - name: Run pytest + # NOTE: + # Pytest (and Alembic) must run from the backend directory so relative paths + # like alembic.ini and script_location resolve correctly. `--project` only + # selects the virtualenv; it does not change the working directory. working-directory: backend run: | - uv run pytest + uv run pytest --strict-markers diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4bf8ac7..0696189 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: # General sanity checks # ------------------------ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -17,16 +17,26 @@ repos: rev: v0.14.10 hooks: - id: ruff-check - args: [--fix] - files: ^backend/ + name: ruff (lint) + args: + - --config=backend/pyproject.toml + - --fix + files: ^backend/.*\.py$ + - id: ruff-format - files: ^backend/ + name: ruff (format) + args: + - --config=backend/pyproject.toml + files: ^backend/.*\.py$ + - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.18.2 + rev: v1.19.1 hooks: - id: mypy name: mypy (type checking) + args: + - --config-file=backend/pyproject.toml files: ^backend/.*\.py$ additional_dependencies: - fastapi diff --git a/README.md b/README.md index 6d26672..22b6742 100644 --- a/README.md +++ b/README.md @@ -251,19 +251,49 @@ Pre-commit runs automatically on `git commit` and will block commits if checks f --- +### ⚠️ Important: Where to run pre-commit + +**Pre-commit must always be run from the repository root.** + +Some hooks (notably `mypy`) reference configuration files using paths relative to the repo root (for example, `backend/pyproject.toml`). Running pre-commit from a subdirectory such as `backend/` can cause those configurations to be missed, leading to inconsistent or misleading results. + +✅ Correct: + +```bash +pre-commit run --all-files +``` + +❌ Incorrect: + +```bash +cd backend +pre-commit run --all-files +``` + +In CI, pre-commit is also executed from the repository root for this reason. + +> **Note:** When using `uv`, CI runs pre-commit via +> `uv run --project backend pre-commit run --all-files`. +> The `--project` flag selects the backend virtual environment, but **does not change the working directory**. Pre-commit itself must still be invoked from the repo root so configuration paths resolve correctly. + +--- + ### What pre-commit checks - **Backend** + - Ruff linting and formatting - - Python style and correctness checks + - Python style and correctness checks (mypy) + - **Frontend** + - ESLint (auto-fix on staged files) - Prettier formatting (staged files only) All hooks are configured in the root `.pre-commit-config.yaml`. -> **Note:** Git hooks run in a non-interactive shell -> Make sure that Node.js tools (such as `pnpm`) are available in your system `PATH` so pre-commit hooks can execute successfully. +> **Note:** Git hooks run in a non-interactive shell. +> Make sure that Node.js tools (such as `pnpm`) are available in your system `PATH` so frontend hooks can execute successfully. --- @@ -293,7 +323,7 @@ make pre-commit-install ### Running pre-commit manually -To run all hooks against all files: +To run all hooks against all files (from the repo root): ```bash make pre-commit-run @@ -302,12 +332,9 @@ make pre-commit-run Or directly: ```bash -cd backend uv run pre-commit run --all-files ``` -You can also run pre-commit from **any directory inside the repo**; it always resolves the repo root configuration. - --- ### Notes & expectations diff --git a/backend/app/api/health.py b/backend/app/api/health.py new file mode 100644 index 0000000..a1e6787 --- /dev/null +++ b/backend/app/api/health.py @@ -0,0 +1,8 @@ +from fastapi import APIRouter + +router = APIRouter(tags=["health"]) + + +@router.get("/health") +async def health(): + return {"status": "ok"} diff --git a/backend/app/api/meta.py b/backend/app/api/meta.py new file mode 100644 index 0000000..834d96c --- /dev/null +++ b/backend/app/api/meta.py @@ -0,0 +1,20 @@ +from fastapi import APIRouter + +from app.api.version import API_VERSION + +router = APIRouter(tags=["meta"]) + + +@router.get("/meta") +def api_meta(): + return { + "version": API_VERSION, + "status": "internal", + "breaking_changes": ( + "No breaking changes are currently declared. This field will describe " + "required upgrade paths when incompatible API versions are introduced." + ), + # Build identifier injected at deploy time (e.g., short git SHA). + # Intentionally None until CI/CD or multi-environment deployments exist. + "build": None, + } diff --git a/backend/app/api/version.py b/backend/app/api/version.py new file mode 100644 index 0000000..beff96d --- /dev/null +++ b/backend/app/api/version.py @@ -0,0 +1,8 @@ +API_PREFIX = "/api" + +# NOTE: Any backward-incompatible response shape change requires a new API version (v2+). +API_VERSION = "v1" + +API_BASE = f"{API_PREFIX}/{API_VERSION}" + +__all__ = ["API_BASE", "API_VERSION"] diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 24c8e48..3002c70 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -16,7 +16,7 @@ def get_env_file(project_root: Path | None = None) -> str | None: Parameters ---------- - project_root : Path or None, optional + project_root : str or Path or None, optional The root directory of the project. If None, it is inferred from the file location. @@ -31,12 +31,13 @@ def get_env_file(project_root: Path | None = None) -> str | None: If the required environment file does not exist. """ # In CI, do not require an env file - if os.getenv("CI", "").lower() == "true": + if os.getenv("CI"): return None app_env = os.getenv("APP_ENV", "dev") - if project_root is None: + # project_root is only specified during testing to point to temp dirs. + if project_root is None: # pragma: no cover project_root = Path(__file__).resolve().parents[3] env_file = project_root / ".envs" / app_env / "backend.env" diff --git a/backend/app/main.py b/backend/app/main.py index 3634503..0b14bba 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,6 +1,9 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware +from app.api.health import router as health_router +from app.api.meta import router as meta_router +from app.api.version import API_BASE from app.core.config import settings from app.core.exceptions import register_exception_handlers from app.core.logger import _setup_root_logger @@ -28,14 +31,12 @@ def create_app() -> FastAPI: ) # Register routers. - app.include_router(simulations_router, prefix="/api") - app.include_router(machine_router, prefix="/api") - app.include_router(user_router, prefix="/api") - app.include_router(auth_router, prefix="/api") - - @app.get("/health") - async def health(): - return {"status": "ok"} + app.include_router(simulations_router, prefix=API_BASE) + app.include_router(machine_router, prefix=API_BASE) + app.include_router(user_router, prefix=API_BASE) + app.include_router(auth_router, prefix=API_BASE) + app.include_router(meta_router, prefix=API_BASE) + app.include_router(health_router, prefix=API_BASE) return app diff --git a/backend/pyproject.toml b/backend/pyproject.toml index a788b73..4cb7f23 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -51,7 +51,7 @@ dev = [ # --- Development Tools --- "ruff==0.14.1", "pre-commit==4.3.0", - "mypy==1.18.2", + "mypy==1.19.1", # --- Runtime / ML --- "torch==2.7.1", @@ -122,13 +122,22 @@ convention = "numpy" [tool.ruff.lint.per-file-ignores] # FastAPI’s use of Depends(...) in defaults is safe and idiomatic. Ignore B008 (do not call a method in argument defaults) in router files. -"app/features/*/api.py" = ["B008"] -"app/features/*/api/*.py" = ["B008"] +"**/app/features/*/api.py" = ["B008"] +"**/app/features/*/api/*.py" = ["B008"] + +[tool.ruff.lint.isort] +known-first-party = ["app"] [tool.pytest.ini_options] # Docs: https://docs.pytest.org/en/7.2.x/reference/customize.html#configuration junit_family = "xunit2" -addopts = "--cov=app --cov-report term --cov-report html:tests_coverage_reports/htmlcov --cov-report xml:tests_coverage_reports/coverage.xml -s" +addopts = [ + "--cov=app", + "--cov-report=term-missing:skip-covered", + "--cov-report=html:tests_coverage_reports/htmlcov", + "--cov-report=xml:tests_coverage_reports/coverage.xml", + "-s", +] python_files = ["tests.py", "test_*.py"] [tool.mypy] diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index b5eaa5d..42b7577 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,6 +1,7 @@ import os import uuid from collections.abc import AsyncGenerator +from pathlib import Path from typing import Generator from urllib.parse import urlparse @@ -26,7 +27,9 @@ logger = _setup_custom_logger(__name__) -ALEMBIC_INI_PATH = "alembic.ini" + +BASE_DIR = Path(__file__).resolve().parents[1] +ALEMBIC_INI_PATH = BASE_DIR / "alembic.ini" # ----------------------------------------------- diff --git a/backend/tests/core/test_config.py b/backend/tests/core/test_config.py index f9d09bd..f13fb70 100644 --- a/backend/tests/core/test_config.py +++ b/backend/tests/core/test_config.py @@ -99,8 +99,9 @@ def test_raises_when_env_file_does_not_exist(self, tmp_path, monkeypatch): with pytest.raises(FileNotFoundError): get_env_file(project_root=root) - def test_returns_none_if_environment_is_ci(self, tmp_path, monkeypatch): - monkeypatch.setenv("CI", "true") + @pytest.mark.parametrize("ci_value", ["true", "1", "yes", "TRUE"]) + def test_returns_none_if_environment_is_ci(self, tmp_path, monkeypatch, ci_value): + monkeypatch.setenv("CI", ci_value) root = tmp_path env_file = get_env_file(project_root=root) diff --git a/backend/tests/features/machine/test_api.py b/backend/tests/features/machine/test_api.py index d7f6b67..7af422a 100644 --- a/backend/tests/features/machine/test_api.py +++ b/backend/tests/features/machine/test_api.py @@ -3,6 +3,7 @@ from fastapi import HTTPException from sqlalchemy.orm import Session +from app.api.version import API_BASE from app.features.machine.api import create_machine, get_machine, list_machines from app.features.machine.models import Machine from app.features.machine.schemas import MachineCreate @@ -35,7 +36,7 @@ def test_endpoint_succeeds_with_valid_payload(self, client): "notes": "Another test machine", } - res = client.post("/api/machines", json=payload) + res = client.post(f"{API_BASE}/machines", json=payload) assert res.status_code == 201 data = res.json() @@ -93,7 +94,7 @@ def test_endpoint_raises_400_for_duplicate_name(self, client, db: Session): "notes": "Duplicate machine", } - res = client.post("/api/machines", json=payload) + res = client.post(f"{API_BASE}/machines", json=payload) assert res.status_code == 400 assert res.json()["detail"] == "Machine with this name already exists" @@ -128,7 +129,7 @@ def test_endpoint_successfully_list_machines(self, client): "chrysalis", } - res = client.get("/api/machines") + res = client.get(f"{API_BASE}/machines") assert res.status_code == 200 data = res.json() @@ -167,7 +168,7 @@ def test_endpoint_successfully_get_machine(self, client, db: Session): db.commit() db.refresh(expected) - res = client.get(f"/api/machines/{expected.id}") + res = client.get(f"{API_BASE}/machines/{expected.id}") assert res.status_code == 200 result_endpoint = res.json() @@ -185,6 +186,6 @@ def test_function_raises_error_if_machine_not_found(self, db: Session): def test_endpoint_raises_404_if_machine_not_found(self, client): random_id = uuid4() - res = client.get(f"/api/machines/{random_id}") + res = client.get(f"{API_BASE}/machines/{random_id}") assert res.status_code == 404 assert res.json()["detail"] == "Machine not found" diff --git a/backend/tests/features/simulation/test_api.py b/backend/tests/features/simulation/test_api.py index 6e60a24..8d21604 100644 --- a/backend/tests/features/simulation/test_api.py +++ b/backend/tests/features/simulation/test_api.py @@ -3,6 +3,7 @@ import pytest from sqlalchemy.orm import Session +from app.api.version import API_BASE from app.features.machine.models import Machine from app.features.simulation.models import Simulation from app.features.user.manager import current_active_user @@ -66,7 +67,7 @@ def test_endpoint_succeeds_with_valid_payload( ], } - res = client.post("/api/simulations", json=payload) + res = client.post(f"{API_BASE}/simulations", json=payload) assert res.status_code == 201 data = res.json() assert data["name"] == payload["name"] @@ -78,7 +79,7 @@ def test_endpoint_succeeds_with_valid_payload( class TestListSimulations: def test_endpoint_returns_empty_list(self, client): - res = client.get("/api/simulations") + res = client.get(f"{API_BASE}/simulations") assert res.status_code == 200 assert res.json() == [] @@ -109,7 +110,7 @@ def test_endpoint_returns_simulations_with_data( db.commit() db.refresh(sim) - res = client.get("/api/simulations") + res = client.get(f"{API_BASE}/simulations") assert res.status_code == 200 data = res.json() assert len(data) == 1 @@ -121,6 +122,8 @@ def test_endpoint_succeeds_with_valid_id( self, client, db: Session, normal_user_sync, admin_user_sync ): machine = db.query(Machine).first() + assert machine is not None, "No machine found in the database" + sim = Simulation( name="Test Simulation", case_name="test_case", @@ -142,11 +145,11 @@ def test_endpoint_succeeds_with_valid_id( db.commit() db.refresh(sim) - res = client.get(f"/api/simulations/{sim.id}") + res = client.get(f"{API_BASE}/simulations/{sim.id}") assert res.status_code == 200 assert res.json()["name"] == sim.name def test_endpoint_raises_404_if_simulation_not_found(self, client): - res = client.get(f"/api/simulations/{uuid4()}") + res = client.get(f"{API_BASE}/simulations/{uuid4()}") assert res.status_code == 404 assert res.json() == {"detail": "Simulation not found"} diff --git a/backend/tests/features/user/test_api.py b/backend/tests/features/user/test_api.py index d61fe35..4ab9f0b 100644 --- a/backend/tests/features/user/test_api.py +++ b/backend/tests/features/user/test_api.py @@ -7,6 +7,7 @@ from fastapi.routing import APIRoute from httpx import AsyncClient +from app.api.version import API_BASE from app.core.config import settings from app.features.user import oauth from app.features.user.models import UserRole @@ -42,7 +43,7 @@ async def test_github_oauth_authorize_redirect( self, async_client: AsyncClient ) -> None: """Ensure the GitHub OAuth authorize endpoint redirects or renders.""" - response = await async_client.get("/api/auth/github/authorize") + response = await async_client.get(f"{API_BASE}/auth/github/authorize") assert response.status_code in ( status.HTTP_200_OK, @@ -68,7 +69,7 @@ async def test_github_oauth_callback_invalid_state( ), ): response = await async_client.get( - "/api/auth/github/callback?code=fake&state=fake" + f"{API_BASE}/auth/github/callback?code=fake&state=fake" ) # Depending on cookie/JWT config, FastAPI-Users may redirect or just 400 @@ -84,7 +85,7 @@ async def test_logout_clears_cookie(self, async_client: AsyncClient) -> None: cookie_name = settings.cookie_name async_client.cookies.set(cookie_name, "fake_cookie_value") - response = await async_client.post("/api/auth/logout") + response = await async_client.post(f"{API_BASE}/auth/logout") assert response.status_code == status.HTTP_200_OK assert response.json() == {"message": "Successfully logged out"} @@ -111,7 +112,7 @@ async def test_logout_clears_cookie(self, async_client: AsyncClient) -> None: @pytest.mark.asyncio async def test_logout_without_cookie(self, async_client: AsyncClient) -> None: """Ensure the logout endpoint works even if no cookie is set.""" - response = await async_client.post("/api/auth/logout") + response = await async_client.post(f"{API_BASE}/auth/logout") assert response.status_code == status.HTTP_200_OK assert response.json() == {"message": "Successfully logged out"} @@ -122,7 +123,7 @@ class TestUserRoutes: async def test_users_me_requires_auth(self, async_client: AsyncClient) -> None: """Unauthenticated request to /users/me should return 401.""" - response = await async_client.get("/api/users/me") + response = await async_client.get(f"{API_BASE}/users/me") assert response.status_code == status.HTTP_401_UNAUTHORIZED @@ -141,9 +142,9 @@ def override_user(): "role": UserRole.USER.value, } - override_dependency("/api/users/me", "current_user", override_user) + override_dependency(f"{API_BASE}/users/me", "current_user", override_user) - response = await async_client.get("/api/users/me") + response = await async_client.get(f"{API_BASE}/users/me") assert response.status_code == 200, response.text data = response.json() diff --git a/backend/tests/test_main.py b/backend/tests/test_main.py index 416aee9..7465d17 100644 --- a/backend/tests/test_main.py +++ b/backend/tests/test_main.py @@ -1,6 +1,7 @@ import pytest from fastapi.testclient import TestClient +from app.api.version import API_BASE from app.main import app @@ -11,9 +12,24 @@ def client(): class TestApp: def test_health_endpoint(self, client): - response = client.get("/health") + response = client.get(f"{API_BASE}/health") + assert response.status_code == 200 assert response.json() == {"status": "ok"} + def test_meta_endpoint(self, client): + response = client.get(f"{API_BASE}/meta") + + assert response.status_code == 200 + assert response.json() == { + "version": "v1", + "status": "internal", + "breaking_changes": ( + "No breaking changes are currently declared. This field will describe " + "required upgrade paths when incompatible API versions are introduced." + ), + "build": None, + } + def test_app_title(self): assert app.title == "SimBoard API" diff --git a/backend/uv.lock b/backend/uv.lock index 1475c5c..32289d7 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -987,6 +987,47 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" }, ] +[[package]] +name = "librt" +version = "0.7.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/29/47f29026ca17f35cf299290292d5f8331f5077364974b7675a353179afa2/librt-0.7.7.tar.gz", hash = "sha256:81d957b069fed1890953c3b9c3895c7689960f233eea9a1d9607f71ce7f00b2c", size = 145910, upload-time = "2026-01-01T23:52:22.87Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/56/30b5c342518005546df78841cb0820ae85a17e7d07d521c10ef367306d0d/librt-0.7.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a487b71fbf8a9edb72a8c7a456dda0184642d99cd007bc819c0b7ab93676a8ee", size = 54709, upload-time = "2026-01-01T23:51:02.774Z" }, + { url = "https://files.pythonhosted.org/packages/72/78/9f120e3920b22504d4f3835e28b55acc2cc47c9586d2e1b6ba04c3c1bf01/librt-0.7.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f4d4efb218264ecf0f8516196c9e2d1a0679d9fb3bb15df1155a35220062eba8", size = 56663, upload-time = "2026-01-01T23:51:03.838Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/7d7a1ee7dfc1151836028eba25629afcf45b56bbc721293e41aa2e9b8934/librt-0.7.7-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b8bb331aad734b059c4b450cd0a225652f16889e286b2345af5e2c3c625c3d85", size = 161705, upload-time = "2026-01-01T23:51:04.917Z" }, + { url = "https://files.pythonhosted.org/packages/45/a5/952bc840ac8917fbcefd6bc5f51ad02b89721729814f3e2bfcc1337a76d6/librt-0.7.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:467dbd7443bda08338fc8ad701ed38cef48194017554f4c798b0a237904b3f99", size = 171029, upload-time = "2026-01-01T23:51:06.09Z" }, + { url = "https://files.pythonhosted.org/packages/fa/bf/c017ff7da82dc9192cf40d5e802a48a25d00e7639b6465cfdcee5893a22c/librt-0.7.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50d1d1ee813d2d1a3baf2873634ba506b263032418d16287c92ec1cc9c1a00cb", size = 184704, upload-time = "2026-01-01T23:51:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/77/ec/72f3dd39d2cdfd6402ab10836dc9cbf854d145226062a185b419c4f1624a/librt-0.7.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c7e5070cf3ec92d98f57574da0224f8c73faf1ddd6d8afa0b8c9f6e86997bc74", size = 180719, upload-time = "2026-01-01T23:51:09.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/86/06e7a1a81b246f3313bf515dd9613a1c81583e6fd7843a9f4d625c4e926d/librt-0.7.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:bdb9f3d865b2dafe7f9ad7f30ef563c80d0ddd2fdc8cc9b8e4f242f475e34d75", size = 174537, upload-time = "2026-01-01T23:51:10.611Z" }, + { url = "https://files.pythonhosted.org/packages/83/08/f9fb2edc9c7a76e95b2924ce81d545673f5b034e8c5dd92159d1c7dae0c6/librt-0.7.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8185c8497d45164e256376f9da5aed2bb26ff636c798c9dabe313b90e9f25b28", size = 195238, upload-time = "2026-01-01T23:51:11.762Z" }, + { url = "https://files.pythonhosted.org/packages/ba/56/ea2d2489d3ea1f47b301120e03a099e22de7b32c93df9a211e6ff4f9bf38/librt-0.7.7-cp311-cp311-win32.whl", hash = "sha256:44d63ce643f34a903f09ff7ca355aae019a3730c7afd6a3c037d569beeb5d151", size = 42939, upload-time = "2026-01-01T23:51:13.192Z" }, + { url = "https://files.pythonhosted.org/packages/58/7b/c288f417e42ba2a037f1c0753219e277b33090ed4f72f292fb6fe175db4c/librt-0.7.7-cp311-cp311-win_amd64.whl", hash = "sha256:7d13cc340b3b82134f8038a2bfe7137093693dcad8ba5773da18f95ad6b77a8a", size = 49240, upload-time = "2026-01-01T23:51:14.264Z" }, + { url = "https://files.pythonhosted.org/packages/7c/24/738eb33a6c1516fdb2dfd2a35db6e5300f7616679b573585be0409bc6890/librt-0.7.7-cp311-cp311-win_arm64.whl", hash = "sha256:983de36b5a83fe9222f4f7dcd071f9b1ac6f3f17c0af0238dadfb8229588f890", size = 42613, upload-time = "2026-01-01T23:51:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/56/72/1cd9d752070011641e8aee046c851912d5f196ecd726fffa7aed2070f3e0/librt-0.7.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a85a1fc4ed11ea0eb0a632459ce004a2d14afc085a50ae3463cd3dfe1ce43fc", size = 55687, upload-time = "2026-01-01T23:51:16.291Z" }, + { url = "https://files.pythonhosted.org/packages/50/aa/d5a1d4221c4fe7e76ae1459d24d6037783cb83c7645164c07d7daf1576ec/librt-0.7.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c87654e29a35938baead1c4559858f346f4a2a7588574a14d784f300ffba0efd", size = 57136, upload-time = "2026-01-01T23:51:17.363Z" }, + { url = "https://files.pythonhosted.org/packages/23/6f/0c86b5cb5e7ef63208c8cc22534df10ecc5278efc0d47fb8815577f3ca2f/librt-0.7.7-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c9faaebb1c6212c20afd8043cd6ed9de0a47d77f91a6b5b48f4e46ed470703fe", size = 165320, upload-time = "2026-01-01T23:51:18.455Z" }, + { url = "https://files.pythonhosted.org/packages/16/37/df4652690c29f645ffe405b58285a4109e9fe855c5bb56e817e3e75840b3/librt-0.7.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1908c3e5a5ef86b23391448b47759298f87f997c3bd153a770828f58c2bb4630", size = 174216, upload-time = "2026-01-01T23:51:19.599Z" }, + { url = "https://files.pythonhosted.org/packages/9a/d6/d3afe071910a43133ec9c0f3e4ce99ee6df0d4e44e4bddf4b9e1c6ed41cc/librt-0.7.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dbc4900e95a98fc0729523be9d93a8fedebb026f32ed9ffc08acd82e3e181503", size = 189005, upload-time = "2026-01-01T23:51:21.052Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/74060a870fe2d9fd9f47824eba6717ce7ce03124a0d1e85498e0e7efc1b2/librt-0.7.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a7ea4e1fbd253e5c68ea0fe63d08577f9d288a73f17d82f652ebc61fa48d878d", size = 183961, upload-time = "2026-01-01T23:51:22.493Z" }, + { url = "https://files.pythonhosted.org/packages/7c/5e/918a86c66304af66a3c1d46d54df1b2d0b8894babc42a14fb6f25511497f/librt-0.7.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef7699b7a5a244b1119f85c5bbc13f152cd38240cbb2baa19b769433bae98e50", size = 177610, upload-time = "2026-01-01T23:51:23.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d7/b5e58dc2d570f162e99201b8c0151acf40a03a39c32ab824dd4febf12736/librt-0.7.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:955c62571de0b181d9e9e0a0303c8bc90d47670a5eff54cf71bf5da61d1899cf", size = 199272, upload-time = "2026-01-01T23:51:25.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/87/8202c9bd0968bdddc188ec3811985f47f58ed161b3749299f2c0dd0f63fb/librt-0.7.7-cp312-cp312-win32.whl", hash = "sha256:1bcd79be209313b270b0e1a51c67ae1af28adad0e0c7e84c3ad4b5cb57aaa75b", size = 43189, upload-time = "2026-01-01T23:51:26.799Z" }, + { url = "https://files.pythonhosted.org/packages/61/8d/80244b267b585e7aa79ffdac19f66c4861effc3a24598e77909ecdd0850e/librt-0.7.7-cp312-cp312-win_amd64.whl", hash = "sha256:4353ee891a1834567e0302d4bd5e60f531912179578c36f3d0430f8c5e16b456", size = 49462, upload-time = "2026-01-01T23:51:27.813Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1f/75db802d6a4992d95e8a889682601af9b49d5a13bbfa246d414eede1b56c/librt-0.7.7-cp312-cp312-win_arm64.whl", hash = "sha256:a76f1d679beccccdf8c1958e732a1dfcd6e749f8821ee59d7bec009ac308c029", size = 42828, upload-time = "2026-01-01T23:51:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/8d/5e/d979ccb0a81407ec47c14ea68fb217ff4315521730033e1dd9faa4f3e2c1/librt-0.7.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4a0b0a3c86ba9193a8e23bb18f100d647bf192390ae195d84dfa0a10fb6244", size = 55746, upload-time = "2026-01-01T23:51:29.828Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/3b65861fb32f802c3783d6ac66fc5589564d07452a47a8cf9980d531cad3/librt-0.7.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5335890fea9f9e6c4fdf8683061b9ccdcbe47c6dc03ab8e9b68c10acf78be78d", size = 57174, upload-time = "2026-01-01T23:51:31.226Z" }, + { url = "https://files.pythonhosted.org/packages/50/df/030b50614b29e443607220097ebaf438531ea218c7a9a3e21ea862a919cd/librt-0.7.7-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9b4346b1225be26def3ccc6c965751c74868f0578cbcba293c8ae9168483d811", size = 165834, upload-time = "2026-01-01T23:51:32.278Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e1/bd8d1eacacb24be26a47f157719553bbd1b3fe812c30dddf121c0436fd0b/librt-0.7.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a10b8eebdaca6e9fdbaf88b5aefc0e324b763a5f40b1266532590d5afb268a4c", size = 174819, upload-time = "2026-01-01T23:51:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/46/7d/91d6c3372acf54a019c1ad8da4c9ecf4fc27d039708880bf95f48dbe426a/librt-0.7.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:067be973d90d9e319e6eb4ee2a9b9307f0ecd648b8a9002fa237289a4a07a9e7", size = 189607, upload-time = "2026-01-01T23:51:34.604Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ac/44604d6d3886f791fbd1c6ae12d5a782a8f4aca927484731979f5e92c200/librt-0.7.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23d2299ed007812cccc1ecef018db7d922733382561230de1f3954db28433977", size = 184586, upload-time = "2026-01-01T23:51:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/5c/26/d8a6e4c17117b7f9b83301319d9a9de862ae56b133efb4bad8b3aa0808c9/librt-0.7.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6b6f8ea465524aa4c7420c7cc4ca7d46fe00981de8debc67b1cc2e9957bb5b9d", size = 178251, upload-time = "2026-01-01T23:51:37.018Z" }, + { url = "https://files.pythonhosted.org/packages/99/ab/98d857e254376f8e2f668e807daccc1f445e4b4fc2f6f9c1cc08866b0227/librt-0.7.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8df32a99cc46eb0ee90afd9ada113ae2cafe7e8d673686cf03ec53e49635439", size = 199853, upload-time = "2026-01-01T23:51:38.195Z" }, + { url = "https://files.pythonhosted.org/packages/7c/55/4523210d6ae5134a5da959900be43ad8bab2e4206687b6620befddb5b5fd/librt-0.7.7-cp313-cp313-win32.whl", hash = "sha256:86f86b3b785487c7760247bcdac0b11aa8bf13245a13ed05206286135877564b", size = 43247, upload-time = "2026-01-01T23:51:39.629Z" }, + { url = "https://files.pythonhosted.org/packages/25/40/3ec0fed5e8e9297b1cf1a3836fb589d3de55f9930e3aba988d379e8ef67c/librt-0.7.7-cp313-cp313-win_amd64.whl", hash = "sha256:4862cb2c702b1f905c0503b72d9d4daf65a7fdf5a9e84560e563471e57a56949", size = 49419, upload-time = "2026-01-01T23:51:40.674Z" }, + { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, +] + [[package]] name = "makefun" version = "1.16.0" @@ -1104,34 +1145,35 @@ wheels = [ [[package]] name = "mypy" -version = "1.18.2" +version = "1.19.1" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c0/77/8f0d0001ffad290cef2f7f216f96c814866248a0b92a722365ed54648e7e/mypy-1.18.2.tar.gz", hash = "sha256:06a398102a5f203d7477b2923dda3634c36727fa5c237d8f859ef90c42a9924b", size = 3448846, upload-time = "2025-09-19T00:11:10.519Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/88/87/cafd3ae563f88f94eec33f35ff722d043e09832ea8530ef149ec1efbaf08/mypy-1.18.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:807d9315ab9d464125aa9fcf6d84fde6e1dc67da0b6f80e7405506b8ac72bc7f", size = 12731198, upload-time = "2025-09-19T00:09:44.857Z" }, - { url = "https://files.pythonhosted.org/packages/0f/e0/1e96c3d4266a06d4b0197ace5356d67d937d8358e2ee3ffac71faa843724/mypy-1.18.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:776bb00de1778caf4db739c6e83919c1d85a448f71979b6a0edd774ea8399341", size = 11817879, upload-time = "2025-09-19T00:09:47.131Z" }, - { url = "https://files.pythonhosted.org/packages/72/ef/0c9ba89eb03453e76bdac5a78b08260a848c7bfc5d6603634774d9cd9525/mypy-1.18.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1379451880512ffce14505493bd9fe469e0697543717298242574882cf8cdb8d", size = 12427292, upload-time = "2025-09-19T00:10:22.472Z" }, - { url = "https://files.pythonhosted.org/packages/1a/52/ec4a061dd599eb8179d5411d99775bec2a20542505988f40fc2fee781068/mypy-1.18.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1331eb7fd110d60c24999893320967594ff84c38ac6d19e0a76c5fd809a84c86", size = 13163750, upload-time = "2025-09-19T00:09:51.472Z" }, - { url = "https://files.pythonhosted.org/packages/c4/5f/2cf2ceb3b36372d51568f2208c021870fe7834cf3186b653ac6446511839/mypy-1.18.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ca30b50a51e7ba93b00422e486cbb124f1c56a535e20eff7b2d6ab72b3b2e37", size = 13351827, upload-time = "2025-09-19T00:09:58.311Z" }, - { url = "https://files.pythonhosted.org/packages/c8/7d/2697b930179e7277529eaaec1513f8de622818696857f689e4a5432e5e27/mypy-1.18.2-cp311-cp311-win_amd64.whl", hash = "sha256:664dc726e67fa54e14536f6e1224bcfce1d9e5ac02426d2326e2bb4e081d1ce8", size = 9757983, upload-time = "2025-09-19T00:10:09.071Z" }, - { url = "https://files.pythonhosted.org/packages/07/06/dfdd2bc60c66611dd8335f463818514733bc763e4760dee289dcc33df709/mypy-1.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33eca32dd124b29400c31d7cf784e795b050ace0e1f91b8dc035672725617e34", size = 12908273, upload-time = "2025-09-19T00:10:58.321Z" }, - { url = "https://files.pythonhosted.org/packages/81/14/6a9de6d13a122d5608e1a04130724caf9170333ac5a924e10f670687d3eb/mypy-1.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a3c47adf30d65e89b2dcd2fa32f3aeb5e94ca970d2c15fcb25e297871c8e4764", size = 11920910, upload-time = "2025-09-19T00:10:20.043Z" }, - { url = "https://files.pythonhosted.org/packages/5f/a9/b29de53e42f18e8cc547e38daa9dfa132ffdc64f7250e353f5c8cdd44bee/mypy-1.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d6c838e831a062f5f29d11c9057c6009f60cb294fea33a98422688181fe2893", size = 12465585, upload-time = "2025-09-19T00:10:33.005Z" }, - { url = "https://files.pythonhosted.org/packages/77/ae/6c3d2c7c61ff21f2bee938c917616c92ebf852f015fb55917fd6e2811db2/mypy-1.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01199871b6110a2ce984bde85acd481232d17413868c9807e95c1b0739a58914", size = 13348562, upload-time = "2025-09-19T00:10:11.51Z" }, - { url = "https://files.pythonhosted.org/packages/4d/31/aec68ab3b4aebdf8f36d191b0685d99faa899ab990753ca0fee60fb99511/mypy-1.18.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a2afc0fa0b0e91b4599ddfe0f91e2c26c2b5a5ab263737e998d6817874c5f7c8", size = 13533296, upload-time = "2025-09-19T00:10:06.568Z" }, - { url = "https://files.pythonhosted.org/packages/9f/83/abcb3ad9478fca3ebeb6a5358bb0b22c95ea42b43b7789c7fb1297ca44f4/mypy-1.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:d8068d0afe682c7c4897c0f7ce84ea77f6de953262b12d07038f4d296d547074", size = 9828828, upload-time = "2025-09-19T00:10:28.203Z" }, - { url = "https://files.pythonhosted.org/packages/5f/04/7f462e6fbba87a72bc8097b93f6842499c428a6ff0c81dd46948d175afe8/mypy-1.18.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:07b8b0f580ca6d289e69209ec9d3911b4a26e5abfde32228a288eb79df129fcc", size = 12898728, upload-time = "2025-09-19T00:10:01.33Z" }, - { url = "https://files.pythonhosted.org/packages/99/5b/61ed4efb64f1871b41fd0b82d29a64640f3516078f6c7905b68ab1ad8b13/mypy-1.18.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ed4482847168439651d3feee5833ccedbf6657e964572706a2adb1f7fa4dfe2e", size = 11910758, upload-time = "2025-09-19T00:10:42.607Z" }, - { url = "https://files.pythonhosted.org/packages/3c/46/d297d4b683cc89a6e4108c4250a6a6b717f5fa96e1a30a7944a6da44da35/mypy-1.18.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3ad2afadd1e9fea5cf99a45a822346971ede8685cc581ed9cd4d42eaf940986", size = 12475342, upload-time = "2025-09-19T00:11:00.371Z" }, - { url = "https://files.pythonhosted.org/packages/83/45/4798f4d00df13eae3bfdf726c9244bcb495ab5bd588c0eed93a2f2dd67f3/mypy-1.18.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a431a6f1ef14cf8c144c6b14793a23ec4eae3db28277c358136e79d7d062f62d", size = 13338709, upload-time = "2025-09-19T00:11:03.358Z" }, - { url = "https://files.pythonhosted.org/packages/d7/09/479f7358d9625172521a87a9271ddd2441e1dab16a09708f056e97007207/mypy-1.18.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7ab28cc197f1dd77a67e1c6f35cd1f8e8b73ed2217e4fc005f9e6a504e46e7ba", size = 13529806, upload-time = "2025-09-19T00:10:26.073Z" }, - { url = "https://files.pythonhosted.org/packages/71/cf/ac0f2c7e9d0ea3c75cd99dff7aec1c9df4a1376537cb90e4c882267ee7e9/mypy-1.18.2-cp313-cp313-win_amd64.whl", hash = "sha256:0e2785a84b34a72ba55fb5daf079a1003a34c05b22238da94fcae2bbe46f3544", size = 9833262, upload-time = "2025-09-19T00:10:40.035Z" }, - { url = "https://files.pythonhosted.org/packages/87/e3/be76d87158ebafa0309946c4a73831974d4d6ab4f4ef40c3b53a385a66fd/mypy-1.18.2-py3-none-any.whl", hash = "sha256:22a1748707dd62b58d2ae53562ffc4d7f8bcc727e8ac7cbc69c053ddc874d47e", size = 2352367, upload-time = "2025-09-19T00:10:15.489Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" }, + { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" }, + { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" }, + { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" }, + { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" }, + { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" }, + { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" }, + { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" }, + { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" }, + { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" }, + { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" }, ] [[package]] @@ -2352,7 +2394,7 @@ requires-dist = [ dev = [ { name = "factory-boy", specifier = "==3.3.3" }, { name = "ipykernel", specifier = "==7.0.0" }, - { name = "mypy", specifier = "==1.18.2" }, + { name = "mypy", specifier = "==1.19.1" }, { name = "pre-commit", specifier = "==4.3.0" }, { name = "pytest", specifier = "==8.4.2" }, { name = "pytest-asyncio", specifier = "==1.2.0" }, diff --git a/frontend/package.json b/frontend/package.json index 90a775b..7f95b51 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -40,6 +40,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", + "dotenv": "^17.2.3", "lucide-react": "^0.514.0", "motion": "^12.23.24", "next-themes": "^0.4.6", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index dbefab1..fbf46aa 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -74,6 +74,9 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 lucide-react: specifier: ^0.514.0 version: 0.514.0(react@19.2.0) @@ -1638,6 +1641,10 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -4432,6 +4439,8 @@ snapshots: dependencies: esutils: 2.0.3 + dotenv@17.2.3: {} + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 diff --git a/frontend/src/api/api.ts b/frontend/src/api/api.ts index c30cc30..f20e9fc 100644 --- a/frontend/src/api/api.ts +++ b/frontend/src/api/api.ts @@ -2,7 +2,14 @@ import axios from 'axios'; import { getAuthenticated } from '@/api/authState'; -const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'https://127.0.0.1:8000/api'; +const rawApiBaseUrl = import.meta.env.VITE_API_BASE_URL; +const API_BASE_URL = rawApiBaseUrl ?? (import.meta.env.DEV ? 'http://localhost:3000' : undefined); + +if (!API_BASE_URL) { + throw new Error( + 'VITE_API_BASE_URL is not defined. Please set it in your environment (e.g., .env file).', + ); +} export type LogoutFn = (opts?: { silent?: boolean }) => void; let onLogout: LogoutFn | null = null; diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8669fd1..2a09926 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,8 @@ import react from '@vitejs/plugin-react'; import fs from 'fs'; import path from 'path'; -import { defineConfig, loadEnv } from 'vite'; +import dotenv from 'dotenv'; +import { defineConfig } from 'vite'; import tsconfigPaths from 'vite-tsconfig-paths'; // --------------------------------------------------------------------- @@ -15,17 +16,27 @@ const appEnv = process.env.APP_ENV ?? 'dev'; // IMPORTANT: We DO NOT pass `appEnv` as the Vite mode, because // Vite modes can ONLY be: development, production, test // --------------------------------------------------------------------- -const envDir = path.resolve(__dirname, `.envs/${appEnv}`); -// rawEnv forced to "development" for local dev server -const rawEnv = loadEnv('development', envDir, ''); +const envFile = path.resolve(__dirname, `../.envs/${appEnv}/frontend.env`); + +// In CI, rely solely on environment variables +if (!process.env.CI) { + if (!fs.existsSync(envFile)) { + throw new Error( + `Environment file '${envFile}' does not exist. ` + + 'Create it or set CI=true to rely on environment variables.', + ); + } + + dotenv.config({ path: envFile }); +} // --------------------------------------------------------------------- // Filter ONLY variables that start with VITE_ // --------------------------------------------------------------------- const viteEnv: Record = {}; -for (const key in rawEnv) { - if (key.startsWith('VITE_')) { - viteEnv[key] = rawEnv[key]; +for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith('VITE_') && value !== undefined) { + viteEnv[key] = value; } }