mirror of
https://github.com/furyhawk/listen.git
synced 2026-07-20 08:25:34 +00:00
chore: Resolve merge conflicts - update actions/cache to v5 on rebased workflows
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
|
||||
@@ -24,18 +24,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY_IMAGE }}
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
uses: docker/setup-qemu-action@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
|
||||
+14
-15
@@ -22,36 +22,35 @@ jobs:
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
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@v5
|
||||
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
|
||||
|
||||
@@ -14,38 +14,27 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v6
|
||||
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@v5
|
||||
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 }} .
|
||||
|
||||
Vendored
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"python.testing.pytestArgs": [
|
||||
"app"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"chat.tools.terminal.autoApprove": {
|
||||
"pytest": true,
|
||||
"uv": true,
|
||||
"true": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# AGENTS.md — Guidance for AI coding agents
|
||||
|
||||
Purpose: short, actionable guidance for coding agents working in this repository.
|
||||
|
||||
Quick facts
|
||||
- **Python:** >= 3.13 (see [pyproject.toml](pyproject.toml)).
|
||||
- **Run app:** `uvicorn app.main:app --reload` (see [app/main.py](app/main.py)).
|
||||
- **Docker:** `docker-compose up -d` (see [docker-compose.yml](docker-compose.yml)).
|
||||
- **Migrations:** `alembic upgrade head` (see [alembic.ini](alembic.ini) and `alembic/`).
|
||||
- **Tests:** `pytest` (configuration in [pyproject.toml](pyproject.toml); tests under `app/tests`).
|
||||
|
||||
What agents should know (concise)
|
||||
- Use the project's README for onboarding steps and environment setup: [README.md](README.md).
|
||||
- Dependencies are declared in `pyproject.toml`; dev extras include `pytest`, `pytest-asyncio`, `pytest-xdist`, and `ruff`.
|
||||
- Tests create temporary databases (see `app/tests/conftest.py`) and rely on `docker-compose` for a PostgreSQL service when needed.
|
||||
- The app entrypoint is `app.main:app`; routers are in `app/api`.
|
||||
- Alembic is configured for async SQLAlchemy—use the repository's alembic folder for migrations.
|
||||
|
||||
Agent behavior recommendations
|
||||
- Prefer linking to existing docs instead of copying long sections; follow "link, don't embed".
|
||||
- When suggesting code edits, reference the exact file(s) and include minimal, focused patches.
|
||||
- Run tests locally with `pytest -q` and use `-k` to target changed areas during iteration.
|
||||
- Respect the project's Python version and typing/mypy strictness.
|
||||
|
||||
Next customizations to consider
|
||||
- Add a small `.github/copilot-instructions.md` if repository owners want per-repo agent role presets.
|
||||
- Create a `/.vscode` or `devcontainer` quickstart note for reproducible developer environments.
|
||||
|
||||
Relevant files
|
||||
- [README.md](README.md)
|
||||
- [pyproject.toml](pyproject.toml)
|
||||
- [app/main.py](app/main.py)
|
||||
- [alembic.ini](alembic.ini)
|
||||
- [app/tests/conftest.py](app/tests/conftest.py)
|
||||
@@ -0,0 +1,30 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v0.1.21] - 2026-05-11
|
||||
|
||||
Released: https://github.com/furyhawk/listen/releases/tag/v0.1.21
|
||||
|
||||
### Added
|
||||
|
||||
- Implement IoT logging endpoint and related models/schemas. (commit: 255a6f0)
|
||||
- Update IoT endpoint and tests for improved functionality and type hints. (commit: 38e71f8)
|
||||
- Add date range tests for pressure and humidity search endpoints. (commit: ce6734d)
|
||||
- Add VSCode settings for pytest and create AGENTS.md for AI coding guidance. (commit: d8fef06)
|
||||
|
||||
### Changed
|
||||
|
||||
- Update README with new live example URL and add isort to development dependencies. (commit: 7ab3fa8)
|
||||
- Bump dependency groups and various dependency updates across the repo. (multiple commits)
|
||||
- Improve code formatting and organization in test files and migration scripts. (commit: 63e946f)
|
||||
|
||||
### Misc
|
||||
|
||||
- Merge dependency and workflow updates from dependabot and maintenance PRs. (multiple commits)
|
||||
- Chore: add development and testing dependencies to `requirements-dev.txt`. (commit: b530a35)
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- (future changes go here)
|
||||
+21
-6
@@ -1,19 +1,34 @@
|
||||
FROM python:3.13.3-slim-bookworm as base
|
||||
FROM python:3.14-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"
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
curl \
|
||||
build-essential \
|
||||
libpq-dev \
|
||||
libffi-dev \
|
||||
python3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Ensure pip/setuptools/wheel and Rust toolchain helpers are up-to-date so
|
||||
# binary wheels are preferred and building from source can succeed when needed.
|
||||
# Upgrade pip and wheel tooling so prebuilt wheels are preferred
|
||||
RUN pip install --upgrade pip setuptools wheel
|
||||
|
||||
# Install Python dependencies (will build wheels if prebuilt wheels are unavailable)
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Install uvicorn server
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
[](https://minimal-fastapi-postgres-template.rafsaf.pl/)
|
||||
[](https://api.furyhawk.lol/)
|
||||
[](https://github.com/rafsaf/minimal-fastapi-postgres-template/blob/main/LICENSE)
|
||||
[](https://docs.python.org/3/whatsnew/3.13.html)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||
[](https://github.com/rafsaf/minimal-fastapi-postgres-template/actions/workflows/tests.yml)
|
||||
[](https://github.com/furyhawk/listen/actions/workflows/type_check.yml)
|
||||
|
||||
_Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added my domain and https only._
|
||||
_Check out online example: https://api.furyhawk.lol/, it's 100% code used in template (docker image) with added my domain and https only._
|
||||
|
||||
# Minimal async FastAPI + PostgreSQL template
|
||||
|
||||
@@ -12,7 +13,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 +39,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 +56,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
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
"""create_iot_tables
|
||||
|
||||
Revision ID: f1e2d3c4b5a6
|
||||
Revises: 670c74d20f60
|
||||
Create Date: 2026-05-11 00:01:00.000000
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "f1e2d3c4b5a6"
|
||||
down_revision = "670c74d20f60"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table(
|
||||
"iot_device",
|
||||
sa.Column("device_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("node", sa.String(length=128), nullable=True),
|
||||
sa.Column(
|
||||
"create_time",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"update_time",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("device_id"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"iot_reading",
|
||||
sa.Column("id", sa.BigInteger(), nullable=False),
|
||||
sa.Column("device_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("sensor_type", sa.String(length=64), nullable=False),
|
||||
sa.Column("value", sa.String(length=128), nullable=False),
|
||||
sa.Column(
|
||||
"create_time",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column(
|
||||
"update_time",
|
||||
sa.DateTime(timezone=True),
|
||||
server_default=sa.text("now()"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["device_id"], ["iot_device.device_id"], ondelete="CASCADE"
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table("iot_reading")
|
||||
op.drop_table("iot_device")
|
||||
# ### end Alembic commands ###
|
||||
+10
-1
@@ -1,7 +1,15 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api import api_messages
|
||||
from app.api.endpoints import auth, humidity, pets, pressure, temperature, users
|
||||
from app.api.endpoints import (
|
||||
auth,
|
||||
humidity,
|
||||
iot,
|
||||
pets,
|
||||
pressure,
|
||||
temperature,
|
||||
users,
|
||||
)
|
||||
|
||||
auth_router = APIRouter()
|
||||
auth_router.include_router(auth.router, prefix="/auth", tags=["auth"])
|
||||
@@ -38,3 +46,4 @@ api_router.include_router(pressure.router, prefix="/pressure", tags=["pressure"]
|
||||
api_router.include_router(
|
||||
temperature.router, prefix="/temperature", tags=["temperature"]
|
||||
)
|
||||
api_router.include_router(iot.router, prefix="/iot", tags=["iot"])
|
||||
|
||||
@@ -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,29 @@ 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),
|
||||
offset: int = Query(0, ge=0),
|
||||
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()).offset(offset).limit(limit)
|
||||
humidity_list = await session.scalars(query)
|
||||
return list(humidity_list.all())
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api import deps
|
||||
from app.models import IoTDevice, IoTReading
|
||||
from app.schemas.requests import IoTLogRequest
|
||||
from app.schemas.responses import IoTLogResponse
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/log",
|
||||
response_model=IoTLogResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
description="Log batch IoT device sensor readings received from MQTT",
|
||||
)
|
||||
async def log_iot_readings(
|
||||
data: IoTLogRequest, session: AsyncSession = Depends(deps.get_session)
|
||||
) -> dict[str, int]:
|
||||
total = 0
|
||||
|
||||
for device in data.devices:
|
||||
existing = await session.scalar(
|
||||
select(IoTDevice).where(IoTDevice.device_id == device.device_id)
|
||||
)
|
||||
if existing is None:
|
||||
new_dev = IoTDevice(device_id=device.device_id)
|
||||
session.add(new_dev)
|
||||
|
||||
for r in device.readings:
|
||||
new_reading = IoTReading(
|
||||
device_id=device.device_id, sensor_type=r.sensor_type, value=r.value
|
||||
)
|
||||
session.add(new_reading)
|
||||
total += 1
|
||||
|
||||
await session.commit()
|
||||
return {"logged_count": total}
|
||||
@@ -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,29 @@ 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),
|
||||
offset: int = Query(0, ge=0),
|
||||
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()).offset(offset).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,29 @@ 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, limit, and offset pagination.",
|
||||
)
|
||||
async def get_temperature(
|
||||
async def search_temperature(
|
||||
session: AsyncSession = Depends(deps.get_session),
|
||||
limit: int = Query(100, ge=1, le=1000),
|
||||
offset: int = Query(0, ge=0),
|
||||
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()).offset(offset).limit(limit)
|
||||
temperature_list = await session.scalars(query)
|
||||
return list(temperature_list.all())
|
||||
|
||||
@@ -86,3 +86,24 @@ class Pressure(Base):
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
pressure: Mapped[str] = mapped_column(String(10), nullable=False)
|
||||
|
||||
|
||||
class IoTDevice(Base):
|
||||
__tablename__ = "iot_device"
|
||||
|
||||
device_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
||||
node: Mapped[str] = mapped_column(String(128), nullable=True)
|
||||
readings: Mapped[list["IoTReading"]] = relationship(back_populates="device")
|
||||
|
||||
|
||||
class IoTReading(Base):
|
||||
__tablename__ = "iot_reading"
|
||||
|
||||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
|
||||
device_id: Mapped[str] = mapped_column(
|
||||
ForeignKey("iot_device.device_id", ondelete="CASCADE"), nullable=False
|
||||
)
|
||||
sensor_type: Mapped[str] = mapped_column(String(64), nullable=False)
|
||||
value: Mapped[str] = mapped_column(String(128), nullable=False)
|
||||
# reading_time is optional — use create_time from Base for recorded time
|
||||
device: Mapped["IoTDevice"] = relationship(back_populates="readings")
|
||||
|
||||
@@ -58,3 +58,19 @@ class MqttCreateRequest(BaseRequest):
|
||||
# "id": "000617968C1EEAD9DB5B00000CFD0E24",
|
||||
# "flags": {"retain": false, "dup": false},
|
||||
# }
|
||||
|
||||
|
||||
class SensorReading(BaseRequest):
|
||||
sensor_type: str
|
||||
value: str
|
||||
timestamp: int | None = None
|
||||
|
||||
|
||||
class DeviceData(BaseRequest):
|
||||
device_id: str
|
||||
readings: list[SensorReading]
|
||||
|
||||
|
||||
class IoTLogRequest(BaseRequest):
|
||||
devices: list[DeviceData]
|
||||
received_at: int | None = None
|
||||
|
||||
@@ -42,3 +42,15 @@ class PressureResponse(BaseResponse):
|
||||
id: int
|
||||
pressure: str
|
||||
update_time: datetime
|
||||
|
||||
|
||||
class IoTReadingResponse(BaseResponse):
|
||||
id: int
|
||||
device_id: str
|
||||
sensor_type: str
|
||||
value: str
|
||||
update_time: datetime
|
||||
|
||||
|
||||
class IoTLogResponse(BaseResponse):
|
||||
logged_count: int
|
||||
|
||||
@@ -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}"}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import types
|
||||
from typing import Any, cast
|
||||
|
||||
import pytest
|
||||
from fastapi import HTTPException, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api import api_messages, deps
|
||||
|
||||
|
||||
class FakeSession:
|
||||
async def scalar(self, _query: Any) -> Any:
|
||||
return None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_current_user_raises_when_user_removed(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
# make verify_jwt_token return an object with .sub
|
||||
monkeypatch.setattr(
|
||||
deps,
|
||||
"verify_jwt_token",
|
||||
lambda token: types.SimpleNamespace(sub="non-existent"),
|
||||
)
|
||||
|
||||
with pytest.raises(HTTPException) as excinfo:
|
||||
await deps.get_current_user(
|
||||
token="ignored", session=cast(AsyncSession, FakeSession())
|
||||
)
|
||||
|
||||
assert excinfo.value.status_code == status.HTTP_401_UNAUTHORIZED
|
||||
assert excinfo.value.detail == api_messages.JWT_ERROR_USER_REMOVED
|
||||
@@ -0,0 +1,56 @@
|
||||
import pytest
|
||||
from fastapi import status
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.main import app
|
||||
from app.models import IoTReading
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_log_iot_readings_single_device(
|
||||
client: AsyncClient, session: AsyncSession
|
||||
) -> None:
|
||||
payload = {
|
||||
"devices": [
|
||||
{
|
||||
"device_id": "dev-1",
|
||||
"readings": [{"sensor_type": "temperature", "value": "25.5"}],
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
response = await client.post(app.url_path_for("log_iot_readings"), json=payload)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json()["logged_count"] == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_log_iot_readings_persists_multiple(
|
||||
client: AsyncClient, session: AsyncSession
|
||||
) -> None:
|
||||
payload = {
|
||||
"devices": [
|
||||
{
|
||||
"device_id": "dev-2",
|
||||
"readings": [
|
||||
{"sensor_type": "temperature", "value": "21.0"},
|
||||
{"sensor_type": "humidity", "value": "60%"},
|
||||
],
|
||||
},
|
||||
{
|
||||
"device_id": "dev-3",
|
||||
"readings": [{"sensor_type": "pressure", "value": "1001"}],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
response = await client.post(app.url_path_for("log_iot_readings"), json=payload)
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
expected_count = sum(len(d["readings"]) for d in payload["devices"])
|
||||
assert response.json()["logged_count"] == expected_count
|
||||
|
||||
rows = await session.scalars(select(IoTReading))
|
||||
all_readings = list(rows.all())
|
||||
assert len(all_readings) >= expected_count
|
||||
@@ -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,45 @@ 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")
|
||||
async def test_search_pressure_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"
|
||||
|
||||
# ensure at least one record exists in this range
|
||||
pressure = Pressure(pressure="1002.0")
|
||||
session.add(pressure)
|
||||
await session.commit()
|
||||
|
||||
response = await client.get(
|
||||
app.url_path_for("search_pressure"),
|
||||
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")
|
||||
@@ -151,7 +196,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 +208,42 @@ 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
|
||||
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_search_humidity_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"
|
||||
|
||||
# ensure at least one record exists in this range
|
||||
humidity = Humidity(humidity="55.0")
|
||||
session.add(humidity)
|
||||
await session.commit()
|
||||
|
||||
response = await client.get(
|
||||
app.url_path_for("search_humidity"),
|
||||
params={"start_date": start_date, "end_date": end_date},
|
||||
)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert isinstance(response.json(), list)
|
||||
|
||||
Generated
-1709
File diff suppressed because it is too large
Load Diff
+33
-31
@@ -1,39 +1,41 @@
|
||||
[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.23"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"alembic>=1.18.3,<2",
|
||||
"asyncpg>=0.31.0,<1",
|
||||
"bcrypt>=5.0.0,<6",
|
||||
"fastapi>=0.136.1,<1",
|
||||
"pydantic[email]>=2.13.4,<3",
|
||||
"pydantic-settings>=2.14.1,<3",
|
||||
"pyjwt>=2.11.0,<3",
|
||||
"python-multipart>=0.0.28,<1",
|
||||
"sqlalchemy[asyncio]>=2.0.46,<3",
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.13"
|
||||
|
||||
alembic = "^1.14.0"
|
||||
asyncpg = "^0.30.0"
|
||||
bcrypt = "^4.3.0"
|
||||
fastapi = "^0.115.10"
|
||||
pydantic = { extras = ["dotenv", "email"], version = "^2.10.6" }
|
||||
pydantic-settings = "^2.8.1"
|
||||
pyjwt = "^2.10.1"
|
||||
python-multipart = "^0.0.20"
|
||||
sqlalchemy = {extras = ["asyncio"], version = "^2.0.38"}
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
coverage = "^7.6.12"
|
||||
freezegun = "^1.5.1"
|
||||
greenlet = "^3.1.1"
|
||||
httpx = "^0.28.1"
|
||||
mypy = "^1.15.0"
|
||||
pre-commit = "^4.0.1"
|
||||
pytest = "^8.3.4"
|
||||
pytest-asyncio = "^0.26.0"
|
||||
pytest-cov = "^6.0.0"
|
||||
pytest-xdist = "^3.6.1"
|
||||
ruff = "^0.9.9"
|
||||
uvicorn = { extras = ["standard"], version = "^0.34.0" }
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"coverage>=7.14.0,<8",
|
||||
"freezegun>=1.5.5,<2",
|
||||
"greenlet>=3.5.0,<4",
|
||||
"httpx>=0.28.1,<1",
|
||||
"mypy>=2.0.0,<3",
|
||||
"pre-commit>=4.6.0,<5",
|
||||
"pytest>=9.0.3,<10",
|
||||
"pytest-asyncio>=1.0.0,<2",
|
||||
"pytest-cov>=7.1.0,<8",
|
||||
"pytest-xdist>=3.8.0,<4",
|
||||
"ruff>=0.15.12,<1",
|
||||
"isort>=8.0.1,<9",
|
||||
"uvicorn[standard]>=0.46.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,27 @@
|
||||
Release v0.1.21
|
||||
|
||||
Changes since v0.1.20:
|
||||
|
||||
- d513c69 your message
|
||||
- c01b419 Merge pull request #86 from furyhawk/dependabot/pip/all-dependencies-242ef55d95
|
||||
- 7ab3fa8 feat: Update README with new live example URL and add isort to development dependencies
|
||||
- debec44 chore(deps): Bump the all-dependencies group across 1 directory with 16 updates
|
||||
- 63e946f refactor: Improve code formatting and organization in test files and migration scripts
|
||||
- 38e71f8 feat: Update IoT endpoint and tests for improved functionality and type hints
|
||||
- 255a6f0 feat: Implement IoT logging endpoint and related models/schemas
|
||||
- ce6734d feat: Add date range tests for pressure and humidity search endpoints
|
||||
- b530a35 chore: Add development and testing dependencies to requirements-dev.txt
|
||||
- d8fef06 Add VSCode settings for pytest and create AGENTS.md for AI coding guidance
|
||||
- 773dedd Merge pull request #82 from furyhawk/dependabot/github_actions/docker/login-action-4
|
||||
- 9f8ac44 Merge pull request #84 from furyhawk/dependabot/github_actions/docker/setup-qemu-action-4
|
||||
- e638924 Merge pull request #83 from furyhawk/dependabot/docker/python-3.15.0a8-slim-bookworm
|
||||
- 2c8f0a4 chore(deps): Bump docker/setup-qemu-action from 3 to 4
|
||||
- 325360e chore(deps): Bump docker/login-action from 3 to 4
|
||||
- e064423 chore(deps): Bump python
|
||||
- 13a7999 Merge pull request #80 from furyhawk/dependabot/pip/all-dependencies-f44031ab6e
|
||||
- 0e578a3 chore: merge main into PR branch, resolving conflicts
|
||||
- 56353bb chore(deps): Bump the all-dependencies group across 1 directory with 50 updates
|
||||
|
||||
Notes:
|
||||
- Tag `v0.1.21` created at HEAD and pushed to origin.
|
||||
- Please verify CI artifacts if needed.
|
||||
@@ -0,0 +1,7 @@
|
||||
Release v0.1.22
|
||||
|
||||
- Fix Dockerfile: switch to `python:3.14-slim-bookworm`, remove Rust toolchain install, and rely on prebuilt wheels where possible.
|
||||
- Install necessary build deps for wheel compilation (`build-essential`, `libpq-dev`, `libffi-dev`, `python3-dev`).
|
||||
- Prepare image build improvements to avoid heavy Rust installs during CI/local builds.
|
||||
|
||||
See CHANGELOG.md for past releases.
|
||||
@@ -0,0 +1,6 @@
|
||||
Release v0.1.23
|
||||
|
||||
- Bump version to 0.1.23.
|
||||
- Small packaging and Dockerfile fixes (Dockerfile parse error fixed).
|
||||
|
||||
See CHANGELOG.md for historical changes.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Development / test dependencies for local setup
|
||||
# Install with: `python -m pip install -r requirements-dev.txt`
|
||||
|
||||
# Testing
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-cov
|
||||
pytest-xdist
|
||||
|
||||
# Tools
|
||||
ruff
|
||||
mypy
|
||||
pre-commit
|
||||
isort
|
||||
|
||||
# HTTP client for tests
|
||||
httpx
|
||||
|
||||
# Coverage helper
|
||||
coverage
|
||||
|
||||
# Uvicorn for local run
|
||||
uvicorn[standard]
|
||||
@@ -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.136.1
|
||||
# via app (pyproject.toml)
|
||||
greenlet==3.5.0
|
||||
# via sqlalchemy
|
||||
idna==3.14
|
||||
# via
|
||||
# anyio
|
||||
# email-validator
|
||||
mako==1.3.12
|
||||
# via alembic
|
||||
markupsafe==3.0.3
|
||||
# via mako
|
||||
pydantic==2.13.4
|
||||
# via
|
||||
# app (pyproject.toml)
|
||||
# fastapi
|
||||
# pydantic-settings
|
||||
pydantic-core==2.46.4
|
||||
# via pydantic
|
||||
pydantic-settings==2.14.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.28
|
||||
# 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