Skip to main content

Asynchronous repository pattern with SQLAlchemy 2.0 and Pydantic 2.0 support.

Project description

A library using the "Repository" pattern for working with a database. The library is based on Sqlalchemy 2.0 (DeclarativeBase). Additionally, work with DTO based on pydantic 2.0 schemas (BaseModel)

Main Components:

DatabaseRepository: This is the main class that provides methods for interacting with the database. It includes methods for creating, reading, updating, and deleting data. The repository supports working with nested models. You just need to describe the model in relationship (sqlalchemy) and pass the instance to the methods of create, update. The library itself will resolve the parent model and child models of any nesting. You can flexibly configure filtering to get models from the database. All filtering is collected in the function DatabaseRepository._get(). You can add global filters for the entire class (switchable parameter), as well as for a specific repository function.

DTORepository: This class provides methods for working with DTOs, including validation and conversion to and from database models. This class is a wrapper over the main DataBaseRepository. In our work we often need to transform models into their representations (DTO), especially when working with FastAPI. This class can do everything a DatabaseRepository can do. He just knows how to work with pydantic schemas. Important: When working with nested models (relationships), the schema must also be nested, where the field name matches the relationship name in the model, and the field must also be an instance of the class BaseModel. Example:

class Model1(OrmBase): <- OrmBase(DeclarativeBase)
    id: Mapped[int]
    name: Mapped[str]
    model2: Mapped["Model2"] = relationship("Model2", back_populates="model1")

class Model2(OrmBase):
    id: Mapped[int]
    name: Mapped[str]
    model1_id: Mapped[int]
    model1: Mapped["Model1"] = relationship("Model1", back_populates="model2")
    
class Schema1(BaseModel):
    id: int
    name: str
class Schema2(BaseModel):
    id: int
    name: str
    model1: Schema1 <- important field name and instance BaseModel

ConfigORM: This class is a global configuration class. You can change the configuration at any time. Limit for outputting records from the database if no local limit is set when working with the repository. Global filters for models. These filters are applied to all queries for all tables. Don't worry, if the table doesn't have such a field, it will simply be discarded and won't give an error. This filter can be enabled or disabled at any time in a repository instance. For example, the most common filter: {'is_active': True}.

Example of work:

from pydantic import BaseModel
from sqlalchemy import Sequence
from sqlalchemy.orm import joinedload

from ormrepo.db_settings import config_orm
from ormrepo.orm import DatabaseRepository, DTORepository
from sqlalchemy.ext.asyncio import AsyncSession, AsyncEngine, create_async_engine, async_sessionmaker

from tests.models import TModel1, TModel1Schema, RelationModel1, TModel1Rel1Schema, RelationModel1Schema
from tests.session import uri

config_orm.configure(limit=100, global_filters={'is_active': True})

engine: AsyncEngine = create_async_engine(uri, echo=True)
sessionmaker = async_sessionmaker(engine)


async def get_session() -> AsyncSession:
    """Session Manager"""
    async with sessionmaker() as session:
        try:
            yield session
            await session.commit()
        except Exception:
            await session.rollback()
            raise
        finally:
            await session.close()

Example of working with a repository:

async def get_models() -> Sequence[TModel1]:
    async with get_session() as session:
        repo = DatabaseRepository(TModel1,
                                  session,
                                  local_filters=[TModel1.name.like("%model%")],
                                  use_global_filters=False)
        res = await repo.get_many(filters=[TModel1.id == 1],
                                  load=[joinedload(TModel1.relation_models)],
                                  relation_filters={RelationModel1: [RelationModel1.id.in_([1, 2, 3])]},
                                  offset=10,
                                  limit=10)
        return res

Example of working with a DTO:

async def get_dto() -> list[BaseModel]:
    async with get_session() as session:
        repo = DTORepository(DatabaseRepository(TModel1, session),
                             TModel1Schema)
        res = await repo.get_many(filters=[TModel1.id > 1])
        return res

Example of creation

async def create_dto() -> TModel1Rel1Schema:
    async with get_session() as session:
        repo = DTORepository(DatabaseRepository(TModel1, session),
                             TModel1Rel1Schema)
        return await repo.create(TModel1Rel1Schema(name='test',
                                                   serial=1,
                                                   relation_models=[
                                                       RelationModel1Schema(),
                                                       RelationModel1Schema(),
                                                       RelationModel1Schema()
                                                   ]))

Example of update

async def update_dto() -> TModel1Rel1Schema:
    async with get_session() as session:
        repo = DTORepository(DatabaseRepository(TModel1, session),
                             TModel1Rel1Schema)
        return await repo.update(1, TModel1Rel1Schema(name='test_new',
                                                      relation_models=None))
"""
Important:
There will be two changes to the databases here.
1: Model TModel1 name changes. Since in the update function, only those fields that are explicitly set are updated.
Because model_dump(exclude_unset=True).
2: Since we explicitly set relation_models, all records from the related table related to this record will be deleted!!!
"""

Project details


Download files

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

Source Distribution

ormrepo-0.5.0.tar.gz (12.7 kB view details)

Uploaded Source

Built Distribution

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

ormrepo-0.5.0-py3-none-any.whl (13.9 kB view details)

Uploaded Python 3

File details

Details for the file ormrepo-0.5.0.tar.gz.

File metadata

  • Download URL: ormrepo-0.5.0.tar.gz
  • Upload date:
  • Size: 12.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.2 CPython/3.12.3 Linux/6.14.0-33-generic

File hashes

Hashes for ormrepo-0.5.0.tar.gz
Algorithm Hash digest
SHA256 7d8fc55ab3b437ed63b86f04b1647304f66bfc4940fa00c0a7e8dce3a3786090
MD5 ea09b086ab4e9b1348287cd32bc02721
BLAKE2b-256 74ea914e0676b50417793c6c871c3ff7586a0dad51d90ade7440cdd4b7bc99c8

See more details on using hashes here.

File details

Details for the file ormrepo-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: ormrepo-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 13.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/2.1.2 CPython/3.12.3 Linux/6.14.0-33-generic

File hashes

Hashes for ormrepo-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e8e89a51a589b076a8cc7b319be090762d502403209bae8745bc70c1d9e17220
MD5 6137f71f4579e7774402faeffe1c0001
BLAKE2b-256 14a8adf584afc7f8b9c98fe16484203f57f319a1bd5fd1725af54b4096610296

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page