Async SQLAlchemy and FastAPI service helpers, with optional S3-compatible uploads.
Project description
approck-services
Small Python helpers for async SQLAlchemy and FastAPI service layers, plus an optional S3-compatible upload helper. The SQLAlchemy pieces build on approck-sqlalchemy-utils (sessions, base models, ordering helpers).
Features
- SQLAlchemy (async): generic
SQLAlchemyServiceandORMSQLAlchemyServicewith CRUD-style helpers, filter dataclasses (__lt,__gt,__in,__isnull), andmake_service_type()to bind a concrete model (and optional filter) to a service class. - FastAPI: thin wrappers that inject
AsyncSessionviaDepends, plusmake_service_type()variants for route dependencies. - Upload (optional):
BaseUploadServicefor uploading bytes, file-like objects, or remote URLs to S3-compatible storage usingaioboto3.
Requirements
- Python 3.10+
- Core dependency:
multimethod(used for@overload-style dispatch).
Optional extras pull in FastAPI, SQLAlchemy utilities, or AWS SDK as needed.
Installation
pip install approck-services
Install optional components:
pip install "approck-services[fastapi]"
pip install "approck-services[sqlalchemy]"
pip install "approck-services[upload]"
With uv in your own project (pick extras you need):
uv add 'approck-services[sqlalchemy,fastapi,upload]'
Usage
Install the pieces you need. Imports below assume approck-services[sqlalchemy]; FastAPI examples also need [fastapi]. Session wiring uses get_session from approck-sqlalchemy-utils (configure the real session in your app; tests often use approck_sqlalchemy_utils.mocks.get_session).
make_service_type(model_cls)
Returns a small async SQLAlchemy service bound to model_cls. It exposes protected helpers such as _find_one, _find, _create, _save, _remove, _update, _delete — you are expected to subclass and add public methods that build select() / update() / delete() statements for your domain.
from sqlalchemy import select
from approck_services.sqlalchemy import make_service_type
from myapp.models import User
class UserService(make_service_type(User)):
async def get_by_email(self, email: str) -> User | None:
return await self._find_one(select(User).where(User.email == email))
For FastAPI, import make_service_type from approck_services.fastapi instead. The generated class takes session: AsyncSession = Depends(get_session) and can be used as a route dependency.
from approck_services.fastapi import make_service_type
from myapp.models import User
UserService = make_service_type(User)
make_service_type(model_cls, filter_cls)
Returns an ORM-shaped service (ORMSQLAlchemyService) with a filter_cls dataclass. Public methods include filter, filter_statement, create, update, update_indirect, delete, find_one, find_one_or_fail. These assume the model has a numeric primary key id (used in find_one / update / delete).
create/update/update_indirectaccept a PydanticBaseModel; fields are mapped withmodel_dump()(andexclude_unset=Trueon update) onto the ORM instance.filter/filter_statement: for each non-Nonefield on the filter dataclass, aWHEREclause is added. Plain fields mean equality on the same-named column onmodel_cls. Suffixes after__(one double underscore) are interpreted as operators on the prefix name:lt,gt,in,isnull(e.g.created_at__lt,id__in,deleted_at__isnull).- Field
order_byis reserved: if present and truthy, it is passed toapprock-sqlalchemy-utilsorder_by.parse.
import dataclasses
from typing import Optional
from approck_services.sqlalchemy import make_service_type
from myapp.models import User
@dataclasses.dataclass
class UserFilter:
email: Optional[str] = None
age__gt: Optional[int] = None
order_by: Optional[str] = None
UserService = make_service_type(User, UserFilter)
The FastAPI variant is the same factory from approck_services.fastapi; filter_statement / create / etc. can be overridden in a subclass when you need selectinload, multi-table writes, or extra Depends. Call super().__init__(session) and keep session=Depends(get_session) aligned with the generated base.
Upload extra
Install approck-services[upload] (pulls in aioboto3). Import approck_services.integrations.upload.BaseUploadService.
BaseUploadService is constructed with AWS-style credentials, region_name, bucket, and optional endpoint_url (MinIO, Cloudflare R2, other S3-compatible APIs). It exposes:
upload_from_bytes(key, body, content_type=None)— upload raw bytes.upload_from_file(key, file_, content_type=None)— upload from a binary file-like object.upload_from_url(url, key=None, prefix=None)— download viaurllib(synchronous fetch) then upload; ifkeyis omitted, the last path segment of the URL is used, optionally prefixed.
If endpoint_url is set, upload methods return a string URL {endpoint_url}/{bucket}/{quoted_key}. If it is None (typical AWS), they return None — build the public URL in your app if needed.
import os
from approck_services.integrations.upload import BaseUploadService
class AppUploadService(BaseUploadService):
def __init__(self) -> None:
super().__init__(
aws_access_key_id=os.environ["AWS_ACCESS_KEY_ID"],
aws_secret_access_key=os.environ["AWS_SECRET_ACCESS_KEY"],
region_name=os.environ.get("AWS_REGION", "us-east-1"),
bucket=os.environ["S3_BUCKET"],
endpoint_url=os.environ.get("S3_ENDPOINT_URL"), # set for MinIO / R2; omit for AWS
)
async def save_avatar(service: AppUploadService, user_id: int, png: bytes) -> str | None:
return await service.upload_from_bytes(
key=f"avatars/{user_id}.png",
body=png,
content_type="image/png",
)
async def mirror_remote(service: AppUploadService, image_url: str) -> str | None:
return await service.upload_from_url(
image_url,
prefix="imports/",
)
Development
See CONTRIBUTING.md for setup, tests, and pull requests.
License
This project is licensed under the MIT License — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file approck_services-1.0.8.tar.gz.
File metadata
- Download URL: approck_services-1.0.8.tar.gz
- Upload date:
- Size: 7.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c082a70a9cb440e243b90b8a8cff9ff1667ba1cc1b6c098a0378d2906de0223e
|
|
| MD5 |
f6e5675382fb59ac8a363422a235af93
|
|
| BLAKE2b-256 |
438700774d4a3bc7daedefefa134426e55995c4acefb32add073b8fdc5cd3e15
|
File details
Details for the file approck_services-1.0.8-py3-none-any.whl.
File metadata
- Download URL: approck_services-1.0.8-py3-none-any.whl
- Upload date:
- Size: 10.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.17 {"installer":{"name":"uv","version":"0.11.17","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
681a9159dc82b696079b0f5deed7ddbdd9d992ab0e5aef743e67c1bd80b2d2a0
|
|
| MD5 |
b912af7b931a4ed816cf7e30f7d26aa4
|
|
| BLAKE2b-256 |
9c34bba17c79650447328111a153a8ad6542db460915d080bc4f4fd54b6967ca
|