SARepo 0.1.7__tar.gz → 0.1.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sarepo-0.1.8/PKG-INFO ADDED
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: SARepo
3
+ Version: 0.1.8
4
+ Summary: Minimal, explicit Repository & Unit-of-Work layer over SQLAlchemy 2.x
5
+ Author: nurbergenovv
6
+ License: MIT
7
+ Requires-Python: >=3.11
8
+ Description-Content-Type: text/markdown
9
+ License-File: LICENSE
10
+ Requires-Dist: SQLAlchemy>=2.0
11
+ Dynamic: license-file
12
+
13
+ # SARepo
14
+
15
+ **Minimal, explicit Repository + Unit of Work layer on top of SQLAlchemy 2.x (sync & async).**
16
+ No magic method names — just clean typed APIs, composable specs, pagination, and optional soft-delete.
17
+
18
+ ---
19
+
20
+ ## 🚀 Quick Start (Sync)
21
+
22
+ ```python
23
+ from sqlalchemy import create_engine, String
24
+ from sqlalchemy.orm import sessionmaker, DeclarativeBase, Mapped, mapped_column
25
+
26
+ from SARepo.sa_repo import SARepository
27
+ from SARepo.base import PageRequest
28
+ from SARepo.specs import and_specs, ilike
29
+
30
+
31
+ class Base(DeclarativeBase):
32
+ pass
33
+
34
+
35
+ class Request(Base):
36
+ __tablename__ = "requests"
37
+
38
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
39
+ title: Mapped[str] = mapped_column(String(255))
40
+ status: Mapped[str] = mapped_column(String(50))
41
+
42
+
43
+ # Setup database
44
+ engine = create_engine("sqlite+pysqlite:///:memory:", echo=False)
45
+ Base.metadata.create_all(engine)
46
+ SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
47
+
48
+ # Usage
49
+ with SessionLocal() as session:
50
+ repo = SARepository(Request, session)
51
+
52
+ # Create
53
+ r = repo.add(Request(title="Hello", status="NEW"))
54
+ session.commit()
55
+
56
+ # Query + Paginate
57
+ page = repo.page(PageRequest(0, 10), spec=ilike(Request.title, "%He%"))
58
+ print(page.total, [i.title for i in page.items])
59
+ ```
60
+
61
+ ---
62
+
63
+ ## ⚙️ Async Quick Start
64
+
65
+ ```python
66
+ import asyncio
67
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
68
+
69
+ from SARepo.sa_repo import SAAsyncRepository
70
+ from SARepo.base import PageRequest
71
+
72
+
73
+ async def main():
74
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
75
+ async with engine.begin() as conn:
76
+ await conn.run_sync(Base.metadata.create_all)
77
+
78
+ SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
79
+
80
+ async with SessionLocal() as session:
81
+ repo = SAAsyncRepository(Request, session)
82
+
83
+ await repo.add(Request(title="Hello", status="NEW"))
84
+ await session.commit()
85
+
86
+ page = await repo.page(PageRequest(0, 10))
87
+ print(page.total)
88
+
89
+
90
+ asyncio.run(main())
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 🧠 Features
96
+
97
+ ✅ **SARepository / SAAsyncRepository**
98
+ - Clean CRUD operations (`get`, `add`, `update`, `remove`, etc.)
99
+ - `page()` method with total count
100
+
101
+ ✅ **Composable Specs**
102
+ - Combine filters easily (`eq`, `ilike`, `and_specs`, `not_deleted`)
103
+
104
+ ✅ **Soft Delete**
105
+ - Automatically filters out rows with `is_deleted=True` if the model has this column
106
+
107
+ ✅ **Typed Protocol Interface**
108
+ - `CrudRepository` protocol for structural typing & IDE autocompletion
109
+
110
+ ✅ **Unit of Work**
111
+ - Minimal sync & async helpers for transactional operations
112
+
113
+ ---
114
+
115
+ ## 🧩 Example: Spec Composition
116
+
117
+ ```python
118
+ from SARepo.specs import eq, ilike, and_specs
119
+
120
+ spec = and_specs(
121
+ eq(Request.status, "NEW"),
122
+ ilike(Request.title, "%Hello%")
123
+ )
124
+
125
+ page = repo.page(PageRequest(0, 10), spec=spec)
126
+ ```
127
+
128
+ ---
129
+
130
+ ## 📜 License
131
+
132
+ **MIT License**
133
+ Copyright (c) 2025
134
+
135
+ ---
136
+
137
+ ## 🧭 Project Structure (Example)
138
+
139
+ ```
140
+ SARepo/
141
+
142
+ ├── sa_repo.py # Core Repository classes (sync & async)
143
+ ├── base.py # Pagination, PageRequest, DTO helpers
144
+ ├── specs.py # Composable query specs
145
+ ├── uow.py # Optional Unit of Work layer
146
+ ├── __init__.py
147
+ ```
148
+
149
+ ---
150
+
151
+ ### ✅ Philosophy
152
+
153
+ - No ORM “magic” — only explicit SQLAlchemy 2.0 APIs
154
+ - One repository → one model
155
+ - Predictable transaction scope
156
+ - Works with Pydantic DTOs or dataclasses
157
+ - 100 % type-safe
sarepo-0.1.8/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # SARepo
2
+
3
+ **Minimal, explicit Repository + Unit of Work layer on top of SQLAlchemy 2.x (sync & async).**
4
+ No magic method names — just clean typed APIs, composable specs, pagination, and optional soft-delete.
5
+
6
+ ---
7
+
8
+ ## 🚀 Quick Start (Sync)
9
+
10
+ ```python
11
+ from sqlalchemy import create_engine, String
12
+ from sqlalchemy.orm import sessionmaker, DeclarativeBase, Mapped, mapped_column
13
+
14
+ from SARepo.sa_repo import SARepository
15
+ from SARepo.base import PageRequest
16
+ from SARepo.specs import and_specs, ilike
17
+
18
+
19
+ class Base(DeclarativeBase):
20
+ pass
21
+
22
+
23
+ class Request(Base):
24
+ __tablename__ = "requests"
25
+
26
+ id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
27
+ title: Mapped[str] = mapped_column(String(255))
28
+ status: Mapped[str] = mapped_column(String(50))
29
+
30
+
31
+ # Setup database
32
+ engine = create_engine("sqlite+pysqlite:///:memory:", echo=False)
33
+ Base.metadata.create_all(engine)
34
+ SessionLocal = sessionmaker(bind=engine, expire_on_commit=False)
35
+
36
+ # Usage
37
+ with SessionLocal() as session:
38
+ repo = SARepository(Request, session)
39
+
40
+ # Create
41
+ r = repo.add(Request(title="Hello", status="NEW"))
42
+ session.commit()
43
+
44
+ # Query + Paginate
45
+ page = repo.page(PageRequest(0, 10), spec=ilike(Request.title, "%He%"))
46
+ print(page.total, [i.title for i in page.items])
47
+ ```
48
+
49
+ ---
50
+
51
+ ## ⚙️ Async Quick Start
52
+
53
+ ```python
54
+ import asyncio
55
+ from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
56
+
57
+ from SARepo.sa_repo import SAAsyncRepository
58
+ from SARepo.base import PageRequest
59
+
60
+
61
+ async def main():
62
+ engine = create_async_engine("sqlite+aiosqlite:///:memory:", echo=False)
63
+ async with engine.begin() as conn:
64
+ await conn.run_sync(Base.metadata.create_all)
65
+
66
+ SessionLocal = async_sessionmaker(bind=engine, expire_on_commit=False)
67
+
68
+ async with SessionLocal() as session:
69
+ repo = SAAsyncRepository(Request, session)
70
+
71
+ await repo.add(Request(title="Hello", status="NEW"))
72
+ await session.commit()
73
+
74
+ page = await repo.page(PageRequest(0, 10))
75
+ print(page.total)
76
+
77
+
78
+ asyncio.run(main())
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 🧠 Features
84
+
85
+ ✅ **SARepository / SAAsyncRepository**
86
+ - Clean CRUD operations (`get`, `add`, `update`, `remove`, etc.)
87
+ - `page()` method with total count
88
+
89
+ ✅ **Composable Specs**
90
+ - Combine filters easily (`eq`, `ilike`, `and_specs`, `not_deleted`)
91
+
92
+ ✅ **Soft Delete**
93
+ - Automatically filters out rows with `is_deleted=True` if the model has this column
94
+
95
+ ✅ **Typed Protocol Interface**
96
+ - `CrudRepository` protocol for structural typing & IDE autocompletion
97
+
98
+ ✅ **Unit of Work**
99
+ - Minimal sync & async helpers for transactional operations
100
+
101
+ ---
102
+
103
+ ## 🧩 Example: Spec Composition
104
+
105
+ ```python
106
+ from SARepo.specs import eq, ilike, and_specs
107
+
108
+ spec = and_specs(
109
+ eq(Request.status, "NEW"),
110
+ ilike(Request.title, "%Hello%")
111
+ )
112
+
113
+ page = repo.page(PageRequest(0, 10), spec=spec)
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 📜 License
119
+
120
+ **MIT License**
121
+ Copyright (c) 2025
122
+
123
+ ---
124
+
125
+ ## 🧭 Project Structure (Example)
126
+
127
+ ```
128
+ SARepo/
129
+
130
+ ├── sa_repo.py # Core Repository classes (sync & async)
131
+ ├── base.py # Pagination, PageRequest, DTO helpers
132
+ ├── specs.py # Composable query specs
133
+ ├── uow.py # Optional Unit of Work layer
134
+ ├── __init__.py
135
+ ```
136
+
137
+ ---
138
+
139
+ ### ✅ Philosophy
140
+
141
+ - No ORM “magic” — only explicit SQLAlchemy 2.0 APIs
142
+ - One repository → one model
143
+ - Predictable transaction scope
144
+ - Works with Pydantic DTOs or dataclasses
145
+ - 100 % type-safe
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Generic, List, Optional, Protocol, Type, TypeVar, runtime_checkable
4
+
5
+ from SARepo.sa_repo import Spec
6
+ from .base import Page, PageRequest
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ @runtime_checkable
12
+ class CrudRepository(Protocol, Generic[T]):
13
+ """
14
+ Контракт CRUD-репозитория для SQLAlchemy-подобной реализации.
15
+ Реализация должна соответствовать сигнатурам ниже.
16
+ """
17
+
18
+ model: Type[T]
19
+
20
+
21
+ def getAll(
22
+ self,
23
+ limit: Optional[int] = None,
24
+ *,
25
+ include_deleted: bool = False,
26
+ order_by=None,
27
+ **filters: Any,
28
+ ) -> List[T]:
29
+ ...
30
+
31
+ def get(self, id_: Any = None, *, include_deleted: bool = False, **filters: Any) -> T:
32
+ """
33
+ Должен вернуть объект T или бросить исключение (например, NotFoundError),
34
+ если объект не найден. Если хочешь Optional — измени сигнатуру.
35
+ """
36
+ ...
37
+
38
+ def try_get(self, id_: Any = None, *, include_deleted: bool = False, **filters: Any) -> Optional[T]:
39
+ ...
40
+
41
+ def add(self, entity: T) -> T:
42
+ ...
43
+
44
+ def update(self, entity: T) -> T:
45
+ ...
46
+
47
+ def remove(self, entity: Optional[T] = None, id: Optional[Any] = None) -> bool:
48
+ ...
49
+
50
+ def delete_by_id(self, id_: Any) -> bool:
51
+ ...
52
+
53
+ def page(
54
+ self,
55
+ page: PageRequest,
56
+ spec: Optional[Spec] = None,
57
+ order_by=None,
58
+ *,
59
+ include_deleted: bool = False,
60
+ ) -> Page[T]:
61
+ ...
62
+
63
+ def get_all_by_column(
64
+ self,
65
+ column_name: str,
66
+ value: Any,
67
+ *,
68
+ limit: Optional[int] = None,
69
+ order_by=None,
70
+ include_deleted: bool = False,
71
+ **extra_filters: Any,
72
+ ) -> list[T]:
73
+ ...
74
+
75
+ def find_all_by_column(
76
+ self,
77
+ column_name: str,
78
+ value: Any,
79
+ *,
80
+ limit: Optional[int] = None,
81
+ order_by=None,
82
+ include_deleted: bool = False,
83
+ **extra_filters: Any,
84
+ ) -> list[T]:
85
+ ...
86
+
87
+ def get_or_create(self, defaults: Optional[dict] = None, **unique_filters: Any) -> tuple[T, bool]:
88
+ ...
89
+
90
+ def raw_query(self, sql: str, params: Optional[dict] = None) -> list[dict]:
91
+ ...
92
+
93
+ def aggregate_avg(self, column_name: str, **filters: Any) -> Optional[float]:
94
+ ...
95
+
96
+ def aggregate_min(self, column_name: str, **filters: Any):
97
+ ...
98
+
99
+ def aggregate_max(self, column_name: str, **filters: Any):
100
+ ...
101
+
102
+ def aggregate_sum(self, column_name: str, **filters: Any):
103
+ ...
104
+
105
+ def count(self, **filters: Any) -> int:
106
+ ...
107
+
108
+ def restore(self, id_: Any) -> bool:
109
+ ...