mirror of
https://github.com/furyhawk/listen.git
synced 2026-07-21 00:45:36 +00:00
chore: migrate project to uv
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 }} .
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,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
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user