Skip to main content

approck-services

CI PyPI version License: MIT

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 SQLAlchemyService and ORMSQLAlchemyService with CRUD-style helpers, filter dataclasses (__lt, __gt, __in, __isnull), and make_service_type() to bind a concrete model (and optional filter) to a service class.
  • FastAPI: thin wrappers that inject AsyncSession via Depends, plus make_service_type() variants for route dependencies.
  • Upload (optional): BaseUploadService for uploading bytes, file-like objects, or remote URLs to S3-compatible storage using aioboto3.

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_indirect accept a Pydantic BaseModel; fields are mapped with model_dump() (and exclude_unset=True on update) onto the ORM instance.
  • filter / filter_statement: for each non-None field on the filter dataclass, a WHERE clause is added. Plain fields mean equality on the same-named column on model_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_by is reserved: if present and truthy, it is passed to approck-sqlalchemy-utils order_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.

Transaction boundary (autocommit)

By default (autocommit = True) every write helper (create, update, update_indirect, delete, and the low-level _save / _update / _delete / _remove) commits its own transaction. This is the historical behaviour and stays the default, so existing code is unaffected.

Set autocommit = False on a service subclass to make it commit-free: writes flush instead of committing, and the caller owns the transaction boundary. Flushing keeps written rows visible to later reads in the same session, so composing several services into one atomic operation no longer needs a commit flag threaded through the call chain.

class UserService(make_service_type(User, UserFilter)):
    autocommit = False


# The caller opens the boundary; both writes commit together or not at all.
async with session.begin():
    user = await UserService(session).create(user_dto)
    await ProfileService(session).create(profile_dto_for(user))

With autocommit = False the service never calls commit() or rollback() — that is the caller's responsibility (a request-level dependency, a consumer handler, a scheduler tick).

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 via urllib (synchronous fetch) then upload; if key is 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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

approck_services-1.1.0.tar.gz (8.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

approck_services-1.1.0-py3-none-any.whl (10.9 kB view details)

Uploaded Python 3

File details

Details for the file approck_services-1.1.0.tar.gz.

File metadata

  • Download URL: approck_services-1.1.0.tar.gz
  • Upload date:
  • Size: 8.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","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

Hashes for approck_services-1.1.0.tar.gz
Algorithm Hash digest
SHA256 e07fe66e88c310ab88c81cd3ae2988159b3334d02ec6e5d8fe056e692006b07b
MD5 87127f697786358d9fb1f3985899d65f
BLAKE2b-256 cbc4ddee7b00264cb6950147b7357a6965511d1fcb29c24260c573b5ddbbc39f

See more details on using hashes here.

File details

Details for the file approck_services-1.1.0-py3-none-any.whl.

File metadata

  • Download URL: approck_services-1.1.0-py3-none-any.whl
  • Upload date:
  • Size: 10.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.0 {"installer":{"name":"uv","version":"0.12.0","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

Hashes for approck_services-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 11c8db5e711a60671a6f18527596e05631b12bf8f08a320f0b35f6cca1707288
MD5 46d0917cf01a8924705fa4a1f840b382
BLAKE2b-256 5e2da91925507b7030dcef73680e03e248f0bb043e7320a6f8fe89d653c5ea5c

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 files

1.0.8

2 files

1.0.7

2 files

1.0.6

2 files

1.0.5

2 files

1.0.4

2 files

1.0.3

2 files

1.0.2

2 files

1.0.1

2 files

1.0.0

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page