chore: migrate project to uv

This commit is contained in:
2026-04-12 22:10:08 +08:00
parent da3a193836
commit d7997450df
10 changed files with 86 additions and 53 deletions
+7 -4
View File
@@ -36,18 +36,21 @@ jobs:
id: cached-uv-dependencies
uses: actions/cache@v4
with:
path: /opt/venv
path: .venv
key: venv-${{ runner.os }}-python-3.13.1-${{ hashFiles('pyproject.toml', 'uv.lock') }}
path: .venv
key: venv-${{ runner.os }}-python-3.13.1-${{ hashFiles('pyproject.toml', 'uv.lock') }}
- name: Install dependencies
- name: Install dependencies
run: |
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: |
uv run pytest
+8 -19
View File
@@ -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 }} .
+3 -3
View File
@@ -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
- [uv](https://docs.astral.sh/uv/), `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>
@@ -64,7 +64,7 @@ cd your_project_name
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
+8 -4
View File
@@ -37,16 +37,20 @@ async def create_new_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)"),
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]:
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())
+8 -4
View File
@@ -37,16 +37,20 @@ async def create_new_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)"),
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]:
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())
+8 -4
View File
@@ -37,16 +37,20 @@ async def create_new_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)"),
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]:
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())
+1 -1
View File
@@ -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}"}
+11 -9
View File
@@ -1,6 +1,6 @@
import pytest
from datetime import datetime, timedelta
import pytest
from fastapi import status
from httpx import AsyncClient
from sqlalchemy.ext.asyncio import AsyncSession
@@ -8,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:
@@ -52,7 +54,7 @@ async def test_search_temperature(
app.url_path_for("search_temperature"),
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) >= 2
assert len(response.json()) >= MIN_EXPECTED_RESULTS
@pytest.mark.asyncio(loop_scope="session")
@@ -62,7 +64,7 @@ async def test_search_temperature_with_limit(
) -> None:
response = await client.get(
app.url_path_for("search_temperature"),
params={"limit": 1}
params={"limit": 1},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) <= 1
@@ -76,10 +78,10 @@ async def test_search_temperature_with_date_range(
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}
params={"start_date": start_date, "end_date": end_date},
)
assert response.status_code == status.HTTP_200_OK
assert isinstance(response.json(), list)
@@ -128,7 +130,7 @@ async def test_search_pressure(
app.url_path_for("search_pressure"),
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) >= 2
assert len(response.json()) >= MIN_EXPECTED_RESULTS
@pytest.mark.asyncio(loop_scope="session")
@@ -138,7 +140,7 @@ async def test_search_pressure_with_limit(
) -> None:
response = await client.get(
app.url_path_for("search_pressure"),
params={"limit": 1}
params={"limit": 1},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) <= 1
@@ -187,7 +189,7 @@ async def test_search_humidity(
app.url_path_for("search_humidity"),
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) >= 2
assert len(response.json()) >= MIN_EXPECTED_RESULTS
@pytest.mark.asyncio(loop_scope="session")
@@ -197,7 +199,7 @@ async def test_search_humidity_with_limit(
) -> None:
response = await client.get(
app.url_path_for("search_humidity"),
params={"limit": 1}
params={"limit": 1},
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) <= 1
+2 -2
View File
@@ -2,14 +2,14 @@
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.14.0,<2",
"asyncpg>=0.30.0,<1",
"bcrypt>=4.3.0,<5",
"fastapi>=0.115.10,<1",
"pydantic>=2.10.6,<3",
"pydantic[email]>=2.10.6,<3",
"pydantic-settings>=2.8.1,<3",
"pyjwt>=2.10.1,<3",
"python-multipart>=0.0.20,<1",
Generated
+30 -3
View File
@@ -48,14 +48,14 @@ wheels = [
[[package]]
name = "app"
version = "0.1.0a0"
version = "0.1.19"
source = { editable = "." }
dependencies = [
{ name = "alembic" },
{ name = "asyncpg" },
{ name = "bcrypt" },
{ name = "fastapi" },
{ name = "pydantic" },
{ name = "pydantic", extra = ["email"] },
{ name = "pydantic-settings" },
{ name = "pyjwt" },
{ name = "python-multipart" },
@@ -90,7 +90,7 @@ requires-dist = [
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.1,<1" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.15.0,<2" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.0.1,<5" },
{ name = "pydantic", specifier = ">=2.10.6,<3" },
{ name = "pydantic", extras = ["email"], specifier = ">=2.10.6,<3" },
{ name = "pydantic-settings", specifier = ">=2.8.1,<3" },
{ name = "pyjwt", specifier = ">=2.10.1,<3" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.4,<9" },
@@ -303,6 +303,28 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
[[package]]
name = "dnspython"
version = "2.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/57666417c0f90f08bcafa776861060426765fdb422eb10212086fb811d26/dnspython-2.8.0.tar.gz", hash = "sha256:181d3c6996452cb1189c4046c61599b84a5a86e099562ffde77d26984ff26d0f", size = 368251, upload-time = "2025-09-07T18:58:00.022Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/5a/18ad964b0086c6e62e2e7500f7edc89e3faa45033c71c1893d34eed2b2de/dnspython-2.8.0-py3-none-any.whl", hash = "sha256:01d9bbc4a2d76bf0db7c1f729812ded6d912bd318d3b1cf81d30c0f845dbf3af", size = 331094, upload-time = "2025-09-07T18:57:58.071Z" },
]
[[package]]
name = "email-validator"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238, upload-time = "2025-08-26T13:09:06.831Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" },
]
[[package]]
name = "execnet"
version = "2.1.2"
@@ -704,6 +726,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" },
]
[package.optional-dependencies]
email = [
{ name = "email-validator" },
]
[[package]]
name = "pydantic-core"
version = "2.41.5"