Package: repka
repkaRepository pattern implementation - isolate db manipulation from domain modelsInstallationVia pip:pip install repkaVia poetry:poetry add repkaUsagerepka.api.BaseRepositoryBaseRepository used to execute sql-queries (viaaiopg & sqlalchemy) and convert sql-rows to/frompydanticmodelsimportsqlalchemyassafromrepka.apiimportAiopgRepository,IdModelfromrepka.utilsimportcreate_async_db_connection# Define pydantic model# It should inherit repka.api.IdModel#   to set id on entity insert, to update entity with id and more# IdModel inherits pydantic.BaseModel and defines int id fieldclassTask(IdModel):title:str# Define sqlachemy table with same model columnsmetadata=sa.MetaData()tasks_table=sa.Table("tasks",metadata,sa.Column("id",sa.Integer,primary_key=True,autoincrement=True),sa.Column("title",sa.String))# Define repository# You should inherit repka.api.BaseRepository and#   set sqlalchemy-table via table property# Kwargs is sql-row data returned by sqlalchemyclassTaskRepo(AiopgRepository[Task]):table=tasks_table# To use the repository you should instantiate it with async sqlalchemy-connectiondb_url="postgresql://postgres@localhost/test"asyncwithcreate_async_db_connection(db_url)asconn:repo=TaskRepo(conn)# Now you can use the repo# Here we select first task with matching titletask=awaitrepo.first(tasks_table.c.title=="My first task")BaseRepository methodsTmeans generic type passed to BaseRepository (e.g.BaseRepository[Task]means that type ofTisTask)Select methodsrepo.first(*filters: BinaryExpression, orders: Optional[Columns])- get first entity matching sqlalchemy {filters} and {orders}; if no entity matches {filters} thenNoneis returnedExample of {filters}:table.c.title == 'test task'- equals to sql where clause:where title = 'test task'Example of {orders}:table.c.title- equals to sql order by clause:order by titlerepo.get_by_ids(entity_ids: List[int])- get all entities whose id in {entity_ids} (same as sqlwhere id in ({entity_ids}))repo.get_by_id(entity_id: int)- get entity with id = {entity_id}repo.get_or_create(filters: Optional[List[BinaryExpression]], defaults: Optional[Dict])- get entity that matches {filters} if no entity found create new entity with {defaults}; return tuple of entity and entity existence flagrepo.get_all(filters: Optional[List[BinaryExpression]], orders: Optional[Columns])- return all entities matching {filters} and {orders}repo.get_all_ids(filters: Optional[List[BinaryExpression]], orders: Optional[Columns])- return ids of entites matching {filters} and {orders}repo.exists(*filters: BinaryExpression)- check that entity matching {filters} exists using sqlcountstatementInsert methodsrepo.insert(entity: T)- insert entity to db table and set id field to the entityrepo.insert_many(entities: List[T])- insert multiple entities and set ids to them in single transactionUpdate methodsrepo.update(entity: T)- updates entity in dbrepo.update_partial(entity: T, **updated_values)- update entity fields via kwargs and update entity fields in dbrepo.update_many(entities: List[T])- update multiple entities in single transactionDelete methodsrepo.delete(*filters: BinaryExpression)- delete entities matching {filters} via sqldeletestatementTo delete all entities passNoneas an arg:repo.delete(None)repo.delete_by_id(entity_id: int)- delete entity with {entity_id}repo.delete_by_ids(entity_ids: List[int])- delete entities whose id in {entity_ids}Other methods & propertiesrepo.serialize(entity: T)- convert {entity} to dict (e.g. ininsertandupdatemethods)repo.deserialize(**kwargs)- convert {kwargs} to entity (e.g. infirstandget_allmethods)repo.execute_in_transaction()- context manager that allows execute multiple queries in transactionExample: delete all old entities and insert new one in single transaction:asyncwithrepo.execute_in_transaction():awaitrepo.delete()awaitrepo.insert(Task(title="New task"))repo.ignore_default- list of entity fields that will be ignored on insert and set after insert if they equal to default field value.
Useful for auto incrementing / default fields like dates or sequence numbersContextVar supportYou can create lazy-connection repositories with context varsfromcontextvarsimportContextVarfromrepka.utilsimportcreate_async_db_connection# Create context var and instantiate repositorydb_connection=ContextVar("db_connection")repo=TaskRepo(db_connection)# Now you should set the context var somewhere (e.g. in middleware)#   And start using the repositoryasyncwithcreate_async_db_connection(db_url)asconn:db_connection.set(conn)awaitrepo.insert(Task(title="New task"))Other sqlalchemy repositoriesFollowing repositories have same api asAiopgRepository(select methods, insert methods, etc.)repka.api.FakeRepo- repository that uses lists instead of database tables, can be used as mockThis repository is implemented partially, because implementing sqlalchemy features (like filters or orders) is hard and pointless for python listsrepka.json_.DictJsonRepoThis kind of repository used to save/load json objects from file:fromrepka.json_importDictJsonReposongs=[{"artist":"Pig Destroyer","title":"Thumbsucker"},{"artist":"Da Menace","title":"Bag of Funk"}]repo=DictJsonRepo()repo.write(songs,"songs.json")assertrepo.read("songs.json")==songsDictJsonRepo methodsrepo.read(filename: str)- read json file with {filename}, return its content as json primitive (list, dict, str, etc.)repo.write(data: T, filename: str)- serialize json primitive {data} and save it to file with {filename}repo.read_or_write_default(filename: str, default_factory: Callable[[], T])- check file with {filename} exists, read its content if exists, execute {default_factory} and write it to file with {filename} otherwiseExample: read data fromtest.jsonor createtest.jsonwith[{"field": "value"}]if no such file:repo=DictJsonRepo()repo.read_or_write_default("test.json",lambda:[{"field":"value"}])DictJsonRepo constructorDictJsonRepo(directory: str)- set directory where files will be read / written; if not set current working directory will be usedExample: read files fromdata/dir:repo=DictJsonRepo("data")repo.read("test.json")# will read "./data/test.json"Development and contributionDependenciesInstall production and development dependencies via poetry:poetry installTestsTo run tests:Setupdatabase urlviaDB_URLenvironment variable (e.g. via .env file)WARNING:Every test run will drop all tables from the dbRun tests viapytestContributionCreate fork/branch for new feature/fix/whatever[Optional] Install pre-commit hooks:pre-commit install(for manual pre-commit run usepre-commit run -a)When you done create pull request and wait for approvalDeployTo deploy new version you need to increment version via bump2version and publish it to PyPI via poetry:bump2version major/minor/patch
poetry publish --buildDon't forget to fill the CHANGELOG.md before release