mirror of
https://github.com/furyhawk/listen.git
synced 2026-07-21 00:45:36 +00:00
chore: merge main into PR branch, resolving conflicts
- Remove poetry.lock (project migrated to uv) - Update pyproject.toml to use uv/hatchling format with bumped dependency versions - Regenerate uv.lock with updated package versions Co-authored-by: furyhawk <831682+furyhawk@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
---
|
||||
description: "Use when: designing or modifying SQLAlchemy models, creating database migrations, altering schema, managing relationships, or working with Alembic"
|
||||
name: "Database & Models Specialist"
|
||||
tools: [read, edit, search, execute]
|
||||
user-invocable: true
|
||||
---
|
||||
|
||||
You are a database and models specialist for the Listen project. Your role is to design, create, and modify SQLAlchemy models, manage Alembic migrations, and maintain database schema integrity following project conventions.
|
||||
|
||||
## Core Conventions
|
||||
|
||||
- **SQLAlchemy 2.0 async patterns**: Use `Mapped`, `mapped_column()`, and declarative syntax
|
||||
- **Base class inheritance**: All models inherit from `Base` which provides `create_time` and `update_time` timestamps automatically
|
||||
- **Naming**: Table names are lowercase singular (e.g. `user_account`, `refresh_token`, `pet`)
|
||||
- **Primary keys**: Typically `BigInteger` for auto-incrementing, `Uuid` for user-facing IDs (stored as strings)
|
||||
- **Relationships**: Use `relationship()` with `back_populates` for bidirectional relationships, include `ondelete="CASCADE"` on foreign keys
|
||||
- **Alembic automation**: After model changes, run `alembic revision --autogenerate -m "description"` then `alembic upgrade head`
|
||||
- **Testing**: Changes to models require corresponding migration tests; minimum 92% coverage
|
||||
|
||||
## Responsibilities
|
||||
|
||||
1. **Model Design**: Create well-structured SQLAlchemy models with proper types, constraints, and relationships
|
||||
2. **Migrations**: Generate, review, and apply Alembic migrations; ensure bidirectional relationships in models
|
||||
3. **Schema Changes**: Modify existing models while maintaining backward compatibility where appropriate
|
||||
4. **Relationship Management**: Set up proper foreign key constraints and ORM relationships
|
||||
5. **Type Safety**: Use SQLAlchemy 2.0 type hints (`Mapped[T]`, `type[T]`) consistently
|
||||
|
||||
## Constraints
|
||||
|
||||
- DO NOT use deprecated SQLAlchemy 1.x patterns (`Column()`, `ForeignKey()` without relationship setup)
|
||||
- DO NOT create models without proper `__tablename__` definition
|
||||
- DO NOT forget bidirectional relationships when creating foreign key relationships
|
||||
- DO NOT modify migration files manually—regenerate them via `alembic revision --autogenerate`
|
||||
- DO NOT skip test coverage when adding new models or relationships
|
||||
|
||||
## Common Tasks
|
||||
|
||||
- **Add a new model**: Create model in `app/models.py`, run Alembic autogenerate, apply migration
|
||||
- **Add a relationship**: Use `relationship()` with `back_populates` on both sides
|
||||
- **Modify field type or constraint**: Update model, autogenerate migration, apply it
|
||||
- **Handle cascading deletes**: Set `ondelete="CASCADE"` on ForeignKey for dependent data
|
||||
|
||||
## Tools to Use
|
||||
|
||||
- **read**: Review `app/models.py`, `alembic/versions/`, database schema
|
||||
- **edit**: Modify models, apply migrations
|
||||
- **search**: Find model references, relationship usage patterns
|
||||
- **execute**: Run Alembic commands (`alembic revision`, `alembic upgrade`)
|
||||
|
||||
## Output Format
|
||||
|
||||
For model changes, always provide:
|
||||
1. Model definition updated (with full class shown)
|
||||
2. Alembic migration command to run
|
||||
3. Brief explanation of schema change
|
||||
+12
-13
@@ -29,29 +29,28 @@ jobs:
|
||||
with:
|
||||
python-version: "3.13.1"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: false
|
||||
virtualenvs-path: /opt/venv
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-poetry-dependencies
|
||||
id: cached-uv-dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /opt/venv
|
||||
key: venv-${{ runner.os }}-python-3.13.1-${{ hashFiles('poetry.lock') }}
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-python-3.13.1-${{ hashFiles('pyproject.toml', 'uv.lock') }}
|
||||
|
||||
- name: Install dependencies and actiavte virtualenv
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --no-interaction --no-root
|
||||
uv sync --all-extras
|
||||
|
||||
- name: Run tests
|
||||
env:
|
||||
SECURITY__JWT_SECRET_KEY: very-not-secret
|
||||
SECURITY__BACKEND_CORS_ORIGINS: "[]"
|
||||
SECURITY__ALLOWED_HOSTS: '["localhost", "127.0.0.1"]'
|
||||
DATABASE__HOSTNAME: localhost
|
||||
DATABASE__USERNAME: postgres
|
||||
DATABASE__PASSWORD: postgres
|
||||
DATABASE__DB: postgres
|
||||
run: |
|
||||
poetry run pytest
|
||||
uv run pytest
|
||||
|
||||
@@ -21,31 +21,20 @@ jobs:
|
||||
with:
|
||||
python-version: "3.13.1"
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: true
|
||||
|
||||
- name: Install Poetry
|
||||
uses: snok/install-poetry@v1
|
||||
with:
|
||||
virtualenvs-create: true
|
||||
virtualenvs-in-project: false
|
||||
virtualenvs-path: /opt/venv
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v3
|
||||
|
||||
- name: Load cached venv
|
||||
id: cached-poetry-dependencies
|
||||
id: cached-uv-dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: /opt/venv
|
||||
key: venv-${{ runner.os }}-python-3.13.1-${{ hashFiles('poetry.lock') }}
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-python-3.13.1-${{ hashFiles('pyproject.toml', 'uv.lock') }}
|
||||
|
||||
- name: Install dependencies and actiavte virtualenv
|
||||
if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
poetry install --no-interaction --no-root
|
||||
uv sync --all-extras
|
||||
|
||||
- name: Run ${{ matrix.check }}
|
||||
run: |
|
||||
poetry run ${{ matrix.check }} .
|
||||
uv run ${{ matrix.check }} .
|
||||
|
||||
+6
-6
@@ -1,16 +1,16 @@
|
||||
FROM python:3.13.3-slim-bookworm as base
|
||||
FROM python:3.14.3-slim-bookworm as base
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
WORKDIR /build
|
||||
|
||||
# Create requirements.txt file
|
||||
FROM base as poetry
|
||||
RUN pip install poetry==1.8.2
|
||||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry export -o /requirements.txt --without-hashes
|
||||
FROM base as deps
|
||||
RUN pip install uv==0.4.26
|
||||
COPY pyproject.toml ./
|
||||
RUN uv pip compile pyproject.toml -o /requirements.txt
|
||||
|
||||
FROM base as common
|
||||
COPY --from=poetry /requirements.txt .
|
||||
COPY --from=deps /requirements.txt .
|
||||
# Create venv, add it to path and install requirements
|
||||
RUN python -m venv /venv
|
||||
ENV PATH="/venv/bin:$PATH"
|
||||
|
||||
@@ -12,7 +12,7 @@ _Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl,
|
||||
- [Features](#features)
|
||||
- [Quickstart](#quickstart)
|
||||
- [1. Create repository from a template](#1-create-repository-from-a-template)
|
||||
- [2. Install dependecies with Poetry](#2-install-dependecies-with-poetry)
|
||||
- [2. Install dependecies with uv](#2-install-dependecies-with-uv)
|
||||
- [3. Setup database and migrations](#3-setup-database-and-migrations)
|
||||
- [4. Now you can run app](#4-now-you-can-run-app)
|
||||
- [5. Activate pre-commit](#5-activate-pre-commit)
|
||||
@@ -38,7 +38,7 @@ _Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl,
|
||||
- [x] Full [Alembic](https://alembic.sqlalchemy.org/en/latest/) migrations setup
|
||||
- [x] Refresh token endpoint (not only access like in official template)
|
||||
- [x] Ready to go Dockerfile with [uvicorn](https://www.uvicorn.org/) webserver as an example
|
||||
- [x] [Poetry](https://python-poetry.org/docs/), `mypy`, `pre-commit` hooks with [ruff](https://github.com/astral-sh/ruff)
|
||||
- [x] [uv](https://docs.astral.sh/uv/), `mypy`, `pre-commit` hooks with [ruff](https://github.com/astral-sh/ruff)
|
||||
- [x] Perfect pytest asynchronous test setup with +40 tests and full coverage
|
||||
|
||||
<br>
|
||||
@@ -55,16 +55,16 @@ _Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl,
|
||||
|
||||
See [docs](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template).
|
||||
|
||||
### 2. Install dependecies with [Poetry](https://python-poetry.org/docs/)
|
||||
### 2. Install dependecies with [uv](https://docs.astral.sh/uv/)
|
||||
|
||||
```bash
|
||||
cd your_project_name
|
||||
|
||||
### Poetry install (python3.13)
|
||||
poetry install
|
||||
### uv install (python3.13)
|
||||
uv sync
|
||||
```
|
||||
|
||||
Note, be sure to use `python3.13` with this template with either poetry or standard venv & pip, if you need to stick to some earlier python version, you should adapt it yourself (remove new versions specific syntax for example `str | int` for python < 3.10)
|
||||
Note, be sure to use `python3.13` with this template with either uv or a standard venv and pip setup. If you need to stick to an earlier Python version, adapt the project accordingly.
|
||||
|
||||
### 3. Setup database and migrations
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -27,15 +29,28 @@ async def create_new_humidity(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/all",
|
||||
"/search",
|
||||
response_model=list[HumidityResponse],
|
||||
status_code=status.HTTP_200_OK,
|
||||
description="Get humidity of the city.",
|
||||
description="Search humidity readings with optional date range and limit.",
|
||||
)
|
||||
async def get_humidity(
|
||||
async def search_humidity(
|
||||
session: AsyncSession = Depends(deps.get_session),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
start_date: datetime | None = Query(
|
||||
None, description="Filter readings from this date (ISO 8601)"
|
||||
),
|
||||
end_date: datetime | None = Query(
|
||||
None, description="Filter readings until this date (ISO 8601)"
|
||||
),
|
||||
) -> list[Humidity]:
|
||||
humidity_list = await session.scalars(
|
||||
select(Humidity).order_by(Humidity.create_time.desc())
|
||||
)
|
||||
query = select(Humidity)
|
||||
|
||||
if start_date:
|
||||
query = query.where(Humidity.create_time >= start_date)
|
||||
if end_date:
|
||||
query = query.where(Humidity.create_time <= end_date)
|
||||
|
||||
query = query.order_by(Humidity.create_time.desc()).limit(limit)
|
||||
humidity_list = await session.scalars(query)
|
||||
return list(humidity_list.all())
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from sqlalchemy import ScalarResult, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -27,15 +29,28 @@ async def create_new_pressure(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/all",
|
||||
"/search",
|
||||
response_model=list[PressureResponse],
|
||||
status_code=status.HTTP_200_OK,
|
||||
description="Get pressure of the city.",
|
||||
description="Search pressure readings with optional date range and limit.",
|
||||
)
|
||||
async def get_pressure(
|
||||
async def search_pressure(
|
||||
session: AsyncSession = Depends(deps.get_session),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
start_date: datetime | None = Query(
|
||||
None, description="Filter readings from this date (ISO 8601)"
|
||||
),
|
||||
end_date: datetime | None = Query(
|
||||
None, description="Filter readings until this date (ISO 8601)"
|
||||
),
|
||||
) -> list[Pressure]:
|
||||
pressure_list: ScalarResult[Pressure] = await session.scalars(
|
||||
select(Pressure).order_by(Pressure.create_time.desc())
|
||||
)
|
||||
query = select(Pressure)
|
||||
|
||||
if start_date:
|
||||
query = query.where(Pressure.create_time >= start_date)
|
||||
if end_date:
|
||||
query = query.where(Pressure.create_time <= end_date)
|
||||
|
||||
query = query.order_by(Pressure.create_time.desc()).limit(limit)
|
||||
pressure_list: ScalarResult[Pressure] = await session.scalars(query)
|
||||
return list(pressure_list.all())
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
@@ -27,15 +29,28 @@ async def create_new_temperature(
|
||||
|
||||
|
||||
@router.get(
|
||||
"/all",
|
||||
"/search",
|
||||
response_model=list[TemperatureResponse],
|
||||
status_code=status.HTTP_200_OK,
|
||||
description="Get temperature of the city.",
|
||||
description="Search temperature readings with optional date range and limit.",
|
||||
)
|
||||
async def get_temperature(
|
||||
async def search_temperature(
|
||||
session: AsyncSession = Depends(deps.get_session),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
start_date: datetime | None = Query(
|
||||
None, description="Filter readings from this date (ISO 8601)"
|
||||
),
|
||||
end_date: datetime | None = Query(
|
||||
None, description="Filter readings until this date (ISO 8601)"
|
||||
),
|
||||
) -> list[Temperature]:
|
||||
temperature_list = await session.scalars(
|
||||
select(Temperature).order_by(Temperature.create_time.desc())
|
||||
)
|
||||
query = select(Temperature)
|
||||
|
||||
if start_date:
|
||||
query = query.where(Temperature.create_time >= start_date)
|
||||
if end_date:
|
||||
query = query.where(Temperature.create_time <= end_date)
|
||||
|
||||
query = query.order_by(Temperature.create_time.desc()).limit(limit)
|
||||
temperature_list = await session.scalars(query)
|
||||
return list(temperature_list.all())
|
||||
|
||||
@@ -138,6 +138,6 @@ async def fixture_default_user(
|
||||
yield default_user
|
||||
|
||||
|
||||
@pytest_asyncio.fixture(name="default_user_headers", scope="function")
|
||||
@pytest.fixture(name="default_user_headers", scope="function")
|
||||
def fixture_default_user_headers(default_user: User) -> dict[str, str]:
|
||||
return {"Authorization": f"Bearer {default_user_access_token}"}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from httpx import AsyncClient
|
||||
@@ -6,6 +8,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from app.main import app
|
||||
from app.models import Humidity, Pressure, Temperature
|
||||
|
||||
MIN_EXPECTED_RESULTS = 2
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_create_new_temperature(client: AsyncClient) -> None:
|
||||
@@ -35,7 +39,7 @@ async def test_create_new_temperature(client: AsyncClient) -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_temperature(
|
||||
async def test_search_temperature(
|
||||
client: AsyncClient,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
@@ -47,22 +51,40 @@ async def test_get_temperature(
|
||||
await session.commit()
|
||||
|
||||
response = await client.get(
|
||||
app.url_path_for("get_temperature"),
|
||||
app.url_path_for("search_temperature"),
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) >= MIN_EXPECTED_RESULTS
|
||||
|
||||
assert response.json() == [
|
||||
{
|
||||
"update_time": temperature1.update_time.isoformat().replace("+00:00", "Z"),
|
||||
"temperature": temperature1.temperature,
|
||||
"id": temperature1.id,
|
||||
},
|
||||
{
|
||||
"update_time": temperature2.update_time.isoformat().replace("+00:00", "Z"),
|
||||
"temperature": temperature2.temperature,
|
||||
"id": temperature2.id,
|
||||
},
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_search_temperature_with_limit(
|
||||
client: AsyncClient,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
response = await client.get(
|
||||
app.url_path_for("search_temperature"),
|
||||
params={"limit": 1},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) <= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_search_temperature_with_date_range(
|
||||
client: AsyncClient,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
now = datetime.utcnow()
|
||||
start_date = (now - timedelta(days=1)).isoformat() + "Z"
|
||||
end_date = (now + timedelta(days=1)).isoformat() + "Z"
|
||||
|
||||
response = await client.get(
|
||||
app.url_path_for("search_temperature"),
|
||||
params={"start_date": start_date, "end_date": end_date},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@@ -93,7 +115,7 @@ async def test_create_new_pressure(client: AsyncClient) -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_pressure(
|
||||
async def test_search_pressure(
|
||||
client: AsyncClient,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
@@ -105,22 +127,23 @@ async def test_get_pressure(
|
||||
await session.commit()
|
||||
|
||||
response = await client.get(
|
||||
app.url_path_for("get_pressure"),
|
||||
app.url_path_for("search_pressure"),
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) >= MIN_EXPECTED_RESULTS
|
||||
|
||||
assert response.json() == [
|
||||
{
|
||||
"update_time": pressure1.update_time.isoformat().replace("+00:00", "Z"),
|
||||
"pressure": pressure1.pressure,
|
||||
"id": pressure1.id,
|
||||
},
|
||||
{
|
||||
"update_time": pressure2.update_time.isoformat().replace("+00:00", "Z"),
|
||||
"pressure": pressure2.pressure,
|
||||
"id": pressure2.id,
|
||||
},
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_search_pressure_with_limit(
|
||||
client: AsyncClient,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
response = await client.get(
|
||||
app.url_path_for("search_pressure"),
|
||||
params={"limit": 1},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) <= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
@@ -151,7 +174,7 @@ async def test_create_new_humidity(client: AsyncClient) -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_get_humidity(
|
||||
async def test_search_humidity(
|
||||
client: AsyncClient,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
@@ -163,19 +186,20 @@ async def test_get_humidity(
|
||||
await session.commit()
|
||||
|
||||
response = await client.get(
|
||||
app.url_path_for("get_humidity"),
|
||||
app.url_path_for("search_humidity"),
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) >= MIN_EXPECTED_RESULTS
|
||||
|
||||
assert response.json() == [
|
||||
{
|
||||
"update_time": humidity1.update_time.isoformat().replace("+00:00", "Z"),
|
||||
"humidity": humidity1.humidity,
|
||||
"id": humidity1.id,
|
||||
},
|
||||
{
|
||||
"update_time": humidity2.update_time.isoformat().replace("+00:00", "Z"),
|
||||
"humidity": humidity2.humidity,
|
||||
"id": humidity2.id,
|
||||
},
|
||||
]
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_search_humidity_with_limit(
|
||||
client: AsyncClient,
|
||||
session: AsyncSession,
|
||||
) -> None:
|
||||
response = await client.get(
|
||||
app.url_path_for("search_humidity"),
|
||||
params={"limit": 1},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert len(response.json()) <= 1
|
||||
|
||||
Generated
-1990
File diff suppressed because it is too large
Load Diff
+32
-31
@@ -1,39 +1,40 @@
|
||||
[tool.poetry]
|
||||
authors = ["admin <admin@example.com>"]
|
||||
[project]
|
||||
authors = [{email = "admin@example.com", name = "admin"}]
|
||||
description = "FastAPI project generated using minimal-fastapi-postgres-template."
|
||||
name = "app"
|
||||
version = "0.1.0-alpha"
|
||||
version = "0.1.19"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"alembic>=1.18.3,<2",
|
||||
"asyncpg>=0.31.0,<1",
|
||||
"bcrypt>=5.0.0,<6",
|
||||
"fastapi>=0.128.0,<1",
|
||||
"pydantic[email]>=2.12.5,<3",
|
||||
"pydantic-settings>=2.12.0,<3",
|
||||
"pyjwt>=2.11.0,<3",
|
||||
"python-multipart>=0.0.22,<1",
|
||||
"sqlalchemy[asyncio]>=2.0.46,<3",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.13"
|
||||
|
||||
alembic = "^1.18.3"
|
||||
asyncpg = "^0.31.0"
|
||||
bcrypt = "^5.0.0"
|
||||
fastapi = "^0.128.0"
|
||||
pydantic = { extras = ["dotenv", "email"], version = "^2.12.5" }
|
||||
pydantic-settings = "^2.12.0"
|
||||
pyjwt = "^2.11.0"
|
||||
python-multipart = "^0.0.22"
|
||||
sqlalchemy = {extras = ["asyncio"], version = "^2.0.46"}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
coverage = "^7.13.2"
|
||||
freezegun = "^1.5.5"
|
||||
greenlet = "^3.3.1"
|
||||
httpx = "^0.28.1"
|
||||
mypy = "^1.19.1"
|
||||
pre-commit = "^4.5.1"
|
||||
pytest = "^8.4.2"
|
||||
pytest-asyncio = "^0.26.0"
|
||||
pytest-cov = "^7.0.0"
|
||||
pytest-xdist = "^3.8.0"
|
||||
ruff = "^0.14.14"
|
||||
uvicorn = { extras = ["standard"], version = "^0.40.0" }
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"coverage>=7.13.2,<8",
|
||||
"freezegun>=1.5.5,<2",
|
||||
"greenlet>=3.3.1,<4",
|
||||
"httpx>=0.28.1,<1",
|
||||
"mypy>=1.19.1,<2",
|
||||
"pre-commit>=4.5.1,<5",
|
||||
"pytest>=8.4.2,<9",
|
||||
"pytest-asyncio>=0.26.0,<1",
|
||||
"pytest-cov>=7.0.0,<8",
|
||||
"pytest-xdist>=3.8.0,<4",
|
||||
"ruff>=0.14.14,<1",
|
||||
"uvicorn[standard]>=0.40.0,<1",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "hatchling.build"
|
||||
requires = ["hatchling"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-vv -n auto --cov --cov-report xml --cov-report term-missing --cov-fail-under=92"
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml -o requirements.txt
|
||||
alembic==1.18.4
|
||||
# via app (pyproject.toml)
|
||||
annotated-doc==0.0.4
|
||||
# via fastapi
|
||||
annotated-types==0.7.0
|
||||
# via pydantic
|
||||
anyio==4.13.0
|
||||
# via starlette
|
||||
asyncpg==0.31.0
|
||||
# via app (pyproject.toml)
|
||||
bcrypt==4.3.0
|
||||
# via app (pyproject.toml)
|
||||
dnspython==2.8.0
|
||||
# via email-validator
|
||||
email-validator==2.3.0
|
||||
# via pydantic
|
||||
fastapi==0.135.3
|
||||
# via app (pyproject.toml)
|
||||
greenlet==3.4.0
|
||||
# via sqlalchemy
|
||||
idna==3.11
|
||||
# via
|
||||
# anyio
|
||||
# email-validator
|
||||
mako==1.3.10
|
||||
# via alembic
|
||||
markupsafe==3.0.3
|
||||
# via mako
|
||||
pydantic==2.12.5
|
||||
# via
|
||||
# app (pyproject.toml)
|
||||
# fastapi
|
||||
# pydantic-settings
|
||||
pydantic-core==2.41.5
|
||||
# via pydantic
|
||||
pydantic-settings==2.13.1
|
||||
# via app (pyproject.toml)
|
||||
pyjwt==2.12.1
|
||||
# via app (pyproject.toml)
|
||||
python-dotenv==1.2.2
|
||||
# via pydantic-settings
|
||||
python-multipart==0.0.26
|
||||
# via app (pyproject.toml)
|
||||
sqlalchemy==2.0.49
|
||||
# via
|
||||
# app (pyproject.toml)
|
||||
# alembic
|
||||
starlette==1.0.0
|
||||
# via fastapi
|
||||
typing-extensions==4.15.0
|
||||
# via
|
||||
# alembic
|
||||
# fastapi
|
||||
# pydantic
|
||||
# pydantic-core
|
||||
# sqlalchemy
|
||||
# typing-inspection
|
||||
typing-inspection==0.4.2
|
||||
# via
|
||||
# fastapi
|
||||
# pydantic
|
||||
# pydantic-settings
|
||||
Reference in New Issue
Block a user