sqlphilosophy 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,360 @@
1
+ """Generic session-bound repository for ORM CRUD."""
2
+
3
+ from __future__ import annotations
4
+ from collections.abc import Sequence
5
+ from typing import Any
6
+ from sqlalchemy import delete
7
+ from sqlalchemy import func
8
+ from sqlalchemy import inspect as sa_inspect
9
+ from sqlalchemy import select
10
+ from sqlalchemy import update
11
+ from sqlalchemy.orm import DeclarativeBase
12
+ from sqlalchemy.orm import Session
13
+ from sqlalchemy.orm.interfaces import LoaderOption
14
+ from sqlphilosophy.sorting import ListQuery
15
+ from sqlphilosophy.sorting import SortConfig
16
+ from sqlphilosophy.sql import apply_mappings_page
17
+ from sqlphilosophy.sql import delete_by_ids_model
18
+ from sqlphilosophy.sql import partial_update_model
19
+ from sqlphilosophy.sql import rows_mapping
20
+ from sqlphilosophy.sync.protocols import RepositoryFactory
21
+ from sqlphilosophy.sync.query import SqlAlchemyStatementBuilder
22
+ from sqlphilosophy.sync.query import StatementQueryBuilder
23
+ from sqlphilosophy.types import IdList
24
+ from sqlphilosophy.types import PrimaryKey
25
+ from sqlphilosophy.types import RowMapping
26
+ from sqlphilosophy.types import SqlBindParams
27
+ from sqlphilosophy.types import SqlSelect
28
+
29
+ LoadRelations = Sequence[LoaderOption]
30
+
31
+
32
+ class BaseRepository[T: DeclarativeBase]:
33
+ """Session-scoped CRUD helpers for a single mapped model."""
34
+
35
+ def __init__(
36
+ self,
37
+ model: type[T],
38
+ session: Session,
39
+ factory: RepositoryFactory | None = None,
40
+ ) -> None:
41
+ self.model = model
42
+ self.session = session
43
+ self._factory = factory
44
+ pk_cols = self.inspect_model(model).primary_key
45
+ if len(pk_cols) != 1:
46
+ raise TypeError(f"{model.__name__} must have a single-column primary key")
47
+ self._pk_column = pk_cols[0]
48
+
49
+ @classmethod
50
+ def inspect_model(cls, model: type[DeclarativeBase]) -> Any:
51
+ """Return SQLAlchemy ORM mapper inspection for ``model``."""
52
+ return sa_inspect(model)
53
+
54
+ def inspect(self) -> Any:
55
+ """Return SQLAlchemy ORM mapper inspection for this repository's model."""
56
+ return self.inspect_model(self.model)
57
+
58
+ def list_table_names(self) -> frozenset[str]:
59
+ """Return visible table names on the session connection."""
60
+ return frozenset(sa_inspect(self.session.connection()).get_table_names())
61
+
62
+ def has_table(self, table_name: str) -> bool:
63
+ """True when ``table_name`` exists on the session connection."""
64
+ return table_name in self.list_table_names()
65
+
66
+ def _apply_load_relations(self, stmt: Any, load_relations: LoadRelations | None) -> Any:
67
+ if load_relations:
68
+ return stmt.options(*load_relations)
69
+ return stmt
70
+
71
+ def _scalar_result(self, stmt: Any, *, unique: bool = False) -> Any:
72
+ if unique:
73
+ return self.session.scalars(stmt).unique()
74
+ return self.session.scalars(stmt)
75
+
76
+ def fetch_statement_mappings(
77
+ self, stmt: Any, params: RowMapping | None = None
78
+ ) -> list[RowMapping]:
79
+ """Execute ``stmt`` and return all rows as mappings."""
80
+ mapped = self.session.execute(stmt, params or {}).mappings()
81
+ rows = mapped.all() if hasattr(mapped, "all") else mapped
82
+ return rows_mapping(rows)
83
+
84
+ def scalar_count(self, stmt: SqlSelect, params: SqlBindParams | None = None) -> int:
85
+ """Execute a scalar count/select statement and return ``int``."""
86
+ return int(self.session.execute(stmt, params or {}).scalar_one())
87
+
88
+ def iter_mappings(self, stmt: SqlSelect, params: SqlBindParams | None = None):
89
+ """Yield each result row as a plain ``dict``."""
90
+ for row in self.session.execute(stmt, params or {}).mappings():
91
+ yield dict(row)
92
+
93
+ def fetch_mapping_first(
94
+ self, stmt: SqlSelect, params: SqlBindParams | None = None
95
+ ) -> RowMapping | None:
96
+ """Execute ``stmt`` and return the first row as a mapping, or ``None``."""
97
+ row = self.session.execute(stmt, params or {}).mappings().first()
98
+ return dict(row) if row is not None else None
99
+
100
+ def fetch_mapping_one(self, stmt: SqlSelect, params: SqlBindParams | None = None) -> RowMapping:
101
+ """Execute ``stmt`` and return exactly one row as a mapping."""
102
+ return dict(self.session.execute(stmt, params or {}).mappings().one())
103
+
104
+ def fetch_mappings_page(
105
+ self,
106
+ stmt: Any,
107
+ *,
108
+ limit: int,
109
+ offset: int,
110
+ params: RowMapping | None = None,
111
+ ) -> list[RowMapping]:
112
+ """Execute ``stmt`` with limit/offset; return normalized row mappings."""
113
+ return apply_mappings_page(
114
+ self.session,
115
+ stmt,
116
+ limit=limit,
117
+ offset=offset,
118
+ params=params,
119
+ )
120
+
121
+ def fetch_sorted_mappings(
122
+ self,
123
+ stmt: Any,
124
+ *,
125
+ list_query: ListQuery,
126
+ params: RowMapping | None = None,
127
+ sort: SortConfig | None = None,
128
+ ) -> list[RowMapping]:
129
+ """Apply optional sort, then return one page of row mappings."""
130
+ if sort is not None:
131
+ stmt = stmt.order_by(*sort.order_clauses(list_query.order_by))
132
+ return self.fetch_mappings_page(
133
+ stmt,
134
+ limit=list_query.limit,
135
+ offset=list_query.offset,
136
+ params=params,
137
+ )
138
+
139
+ def get_by_id(
140
+ self, obj_id: PrimaryKey, load_relations: LoadRelations | None = None
141
+ ) -> T | None:
142
+ """Fetch a single record by primary key with optional eager loading."""
143
+ stmt = select(self.model).where(self._pk_column == obj_id)
144
+ stmt = self._apply_load_relations(stmt, load_relations)
145
+ return self.session.scalar(stmt)
146
+
147
+ def exists(self, obj_id: PrimaryKey) -> bool:
148
+ """True when a row exists for the primary key."""
149
+ return self.get_by_id(obj_id) is not None
150
+
151
+ def exists_where(self, **filters: object) -> bool:
152
+ """True when at least one row matches optional equality filters."""
153
+ return self.count(**filters) > 0
154
+
155
+ def count(self, **filters: object) -> int:
156
+ """Count rows matching optional equality filters."""
157
+ stmt = select(func.count()).select_from(self.model)
158
+ if filters:
159
+ stmt = stmt.filter_by(**filters)
160
+ return int(self.session.scalar(stmt) or 0)
161
+
162
+ def first(self, load_relations: LoadRelations | None = None, **filters: object) -> T | None:
163
+ """Return the first row matching filters, with optional eager loading."""
164
+ stmt = select(self.model).filter_by(**filters).limit(1)
165
+ stmt = self._apply_load_relations(stmt, load_relations)
166
+ return self.session.scalar(stmt)
167
+
168
+ def get(self, obj_id: PrimaryKey, load_relations: LoadRelations | None = None) -> T:
169
+ """Fetch a single record by primary key; raise if missing."""
170
+ obj = self.get_by_id(obj_id, load_relations=load_relations)
171
+ if obj is None:
172
+ raise LookupError(f"{self.model.__name__} matching id={obj_id!r} not found")
173
+ return obj
174
+
175
+ def get_many(
176
+ self, ids: Sequence[PrimaryKey], load_relations: LoadRelations | None = None
177
+ ) -> Sequence[T]:
178
+ """Fetch multiple records by primary key."""
179
+ if not ids:
180
+ return []
181
+ stmt = select(self.model).where(self._pk_column.in_(ids))
182
+ stmt = self._apply_load_relations(stmt, load_relations)
183
+ return self._scalar_result(stmt, unique=load_relations is not None).all()
184
+
185
+ def filter(
186
+ self,
187
+ *,
188
+ page: int = 1,
189
+ limit: int | None = None,
190
+ load_relations: LoadRelations | None = None,
191
+ **filters: object,
192
+ ) -> Sequence[T]:
193
+ """Return rows matching optional equality filters, optionally paginated."""
194
+ if page < 1:
195
+ raise ValueError("page must be >= 1")
196
+ if limit is not None and limit < 1:
197
+ raise ValueError("limit must be >= 1")
198
+ stmt = select(self.model).filter_by(**filters).order_by(self._pk_column)
199
+ if limit is not None:
200
+ stmt = stmt.limit(limit).offset((page - 1) * limit)
201
+ stmt = self._apply_load_relations(stmt, load_relations)
202
+ return self._scalar_result(stmt, unique=load_relations is not None).all()
203
+
204
+ def get_all(
205
+ self,
206
+ *,
207
+ page: int = 1,
208
+ limit: int | None = None,
209
+ load_relations: LoadRelations | None = None,
210
+ ) -> Sequence[T]:
211
+ """Fetch records for this model type, optionally paginated by ``page`` and ``limit``."""
212
+ if page < 1:
213
+ raise ValueError("page must be >= 1")
214
+ if limit is not None and limit < 1:
215
+ raise ValueError("limit must be >= 1")
216
+ statement = select(self.model).order_by(self._pk_column)
217
+ if limit is not None:
218
+ statement = statement.limit(limit).offset((page - 1) * limit)
219
+ statement = self._apply_load_relations(statement, load_relations)
220
+ return self._scalar_result(statement, unique=load_relations is not None).all()
221
+
222
+ def get_with_join(
223
+ self,
224
+ target_model: type[Any],
225
+ *filter_expressions: Any,
226
+ join_on: Any = None,
227
+ ) -> Sequence[tuple[T, Any]]:
228
+ """Explicit INNER JOIN returning ``(base_row, target_row)`` tuples."""
229
+ stmt = select(self.model, target_model)
230
+ if join_on is not None:
231
+ stmt = stmt.join(target_model, join_on)
232
+ else:
233
+ stmt = stmt.join(target_model) # pragma: no cover
234
+ if filter_expressions:
235
+ stmt = stmt.where(*filter_expressions)
236
+ return self.session.execute(stmt).all()
237
+
238
+ def create(self, **fields: object) -> T:
239
+ """Construct, stage, and flush a new instance."""
240
+ return self.add(self.model(**fields))
241
+
242
+ def get_or_create(
243
+ self,
244
+ *,
245
+ defaults: RowMapping | None = None,
246
+ **lookup: object,
247
+ ) -> tuple[T, bool]:
248
+ """Return ``(instance, created)`` for equality ``lookup`` filters."""
249
+ existing = self.first(**lookup)
250
+ if existing is not None:
251
+ return existing, False
252
+ payload: RowMapping = {**(defaults or {}), **lookup}
253
+ return self.create(**payload), True
254
+
255
+ def add(self, obj: T) -> T:
256
+ """Stage a new instance; caller commits in the orchestration layer."""
257
+ self.session.add(obj)
258
+ self.session.flush()
259
+ return obj
260
+
261
+ def update_partial(
262
+ self,
263
+ obj_id: PrimaryKey,
264
+ fields: RowMapping,
265
+ writable: frozenset[str],
266
+ *,
267
+ touch_updated_on: bool = False,
268
+ ) -> int:
269
+ """Apply a partial update; returns affected row count (0 if none)."""
270
+ return partial_update_model(
271
+ self.session,
272
+ self.model,
273
+ obj_id,
274
+ fields,
275
+ writable,
276
+ touch_updated_on=touch_updated_on,
277
+ )
278
+
279
+ def update_where(
280
+ self,
281
+ *,
282
+ criteria: Sequence[object],
283
+ values: RowMapping,
284
+ params: SqlBindParams | None = None,
285
+ ) -> int:
286
+ """Bulk UPDATE rows matching ``criteria``; returns affected row count."""
287
+ if not values:
288
+ return 0
289
+ stmt = update(self.model).where(*criteria).values(**values)
290
+ result = self.session.execute(stmt, params or {})
291
+ return int(result.rowcount or 0)
292
+
293
+ def delete_where(
294
+ self,
295
+ *,
296
+ criteria: Sequence[object],
297
+ params: SqlBindParams | None = None,
298
+ ) -> int:
299
+ """Delete rows matching ``criteria`` via PK lookup + ``delete_many``."""
300
+ if not criteria:
301
+ return 0
302
+ pk_key = self._pk_column.key
303
+ builder = self.statement().select_columns(self._pk_column).where(*criteria)
304
+ if params:
305
+ builder = builder.with_params(params)
306
+ rows = builder.mappings().all()
307
+ ids = [row[pk_key] for row in rows]
308
+ return self.delete_many(ids)
309
+
310
+ def remove(self, obj_id: PrimaryKey) -> bool:
311
+ """Delete a record by primary key."""
312
+ statement = delete(self.model).where(self._pk_column == obj_id)
313
+ result = self.session.execute(statement)
314
+ return bool(result.rowcount)
315
+
316
+ def delete_many(self, ids: IdList) -> int:
317
+ """Delete multiple records by primary key."""
318
+ return delete_by_ids_model(self.session, self.model, list(ids))
319
+
320
+ def delete_all(self) -> int:
321
+ """Delete every row for this model. Dev/ops only — prefer ``delete_where`` in app code."""
322
+ result = self.session.execute(delete(self.model))
323
+ return int(result.rowcount or 0)
324
+
325
+ def batched_purge_ids(
326
+ self,
327
+ *,
328
+ criteria: list[object],
329
+ batch_size: int,
330
+ ) -> int:
331
+ """Delete rows matching ``criteria`` in ``batch_size`` chunks, committing each batch."""
332
+ pk_key = self._pk_column.key
333
+ total = 0
334
+ while True:
335
+ rows = (
336
+ self.statement()
337
+ .select_columns(self._pk_column)
338
+ .where(*criteria)
339
+ .limit(batch_size)
340
+ .mappings()
341
+ .all()
342
+ )
343
+ ids = [row[pk_key] for row in rows]
344
+ if not ids:
345
+ break
346
+ total += self.delete_many(ids)
347
+ self.session.commit()
348
+ return total
349
+
350
+ def statement(self) -> StatementQueryBuilder[T]:
351
+ """Return a fluent statement builder for reads on this model (default read path)."""
352
+ if self._factory is not None:
353
+ return self._factory.create_statement(self.model)
354
+ return SqlAlchemyStatementBuilder(self.session, self.model)
355
+
356
+ def for_repo[R](self, repo_class: type[R]) -> R:
357
+ """Return a typed entity repository sharing this session and factory."""
358
+ if self._factory is None:
359
+ raise RuntimeError("for_repo() requires a RepositoryFactory")
360
+ return self._factory.get_repository(repo_class)
sqlphilosophy/types.py ADDED
@@ -0,0 +1,61 @@
1
+ """Portable SQLAlchemy repository typing aliases (PyPI-safe, no app imports)."""
2
+
3
+ from __future__ import annotations
4
+ from collections.abc import Mapping
5
+ from datetime import date
6
+ from datetime import datetime
7
+ from uuid import UUID
8
+ from sqlalchemy.orm import DeclarativeBase
9
+ from sqlalchemy.sql import ColumnElement
10
+ from sqlalchemy.sql import Select
11
+ from sqlalchemy.sql.selectable import FromClause
12
+ from sqlalchemy.sql.selectable import LateralFromClause
13
+ from sqlalchemy.sql.selectable import ScalarSelect
14
+ from sqlalchemy.sql.selectable import TableClause
15
+
16
+ __all__ = [
17
+ "ApiObject",
18
+ "ApiScalar",
19
+ "IdList",
20
+ "JSONObject",
21
+ "JSONScalar",
22
+ "JSONValue",
23
+ "OrmModel",
24
+ "PrimaryKey",
25
+ "RowMapping",
26
+ "RowValue",
27
+ "SqlBindParams",
28
+ "SqlFilter",
29
+ "SqlFilters",
30
+ "SqlFromClause",
31
+ "SqlLateral",
32
+ "SqlOrderColumn",
33
+ "SqlScalarSubquery",
34
+ "SqlSelect",
35
+ "SqlTable",
36
+ ]
37
+
38
+ type JSONScalar = str | int | float | bool | None
39
+ type JSONValue = JSONScalar | list[JSONValue] | dict[str, JSONValue]
40
+ type JSONObject = dict[str, JSONValue]
41
+
42
+ type ApiScalar = JSONScalar | datetime | date | UUID
43
+ type RowValue = JSONScalar | datetime | date | UUID | bytes | dict[str, RowValue] | list[RowValue]
44
+ type ApiObject = dict[str, RowValue]
45
+
46
+ type PrimaryKey = int | str | UUID
47
+ type IdList = list[PrimaryKey]
48
+
49
+ type SqlBindParams = dict[str, RowValue]
50
+ type SqlFilter = ColumnElement[bool]
51
+ type SqlFilters = list[SqlFilter]
52
+ type SqlOrderColumn = ColumnElement[object]
53
+ type SqlSelect = Select[tuple[object, ...]]
54
+ type SqlFromClause = FromClause
55
+ type SqlLateral = LateralFromClause
56
+ type SqlScalarSubquery = ScalarSelect[object]
57
+ type SqlTable = TableClause
58
+
59
+ type RowMapping = Mapping[str, RowValue]
60
+
61
+ type OrmModel = type[DeclarativeBase]
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: sqlphilosophy
3
+ Version: 0.1.0
4
+ Summary: Portable SQLAlchemy repository kit: sync and async CRUD, statement builders, sort/pagination, and SQL helpers.
5
+ Author-email: Josh Martin <denverprogrammer@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/SignalSafeSoftware/sqlphilosophy
8
+ Project-URL: Repository, https://github.com/SignalSafeSoftware/sqlphilosophy
9
+ Project-URL: Documentation, https://github.com/SignalSafeSoftware/sqlphilosophy#readme
10
+ Project-URL: Issues, https://github.com/SignalSafeSoftware/sqlphilosophy/issues
11
+ Project-URL: Changelog, https://github.com/SignalSafeSoftware/sqlphilosophy/blob/main/CHANGELOG.md
12
+ Keywords: sqlalchemy,repository-pattern,repository,orm,database,pagination,audit,async
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Database
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: <4.0,>=3.12
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: sqlalchemy<3,>=2.0
24
+ Provides-Extra: async
25
+ Requires-Dist: greenlet>=3.0; extra == "async"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8; extra == "dev"
28
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
29
+ Requires-Dist: pytest-cov>=5; extra == "dev"
30
+ Requires-Dist: aiosqlite>=0.20; extra == "dev"
31
+ Requires-Dist: greenlet>=3.0; extra == "dev"
32
+ Requires-Dist: build>=1.2; extra == "dev"
33
+ Requires-Dist: twine>=6.1; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # sqlphilosophy
37
+
38
+ Portable SQLAlchemy repository kit for typed CRUD, fluent statement building, sort/pagination, and Core SQL helpers — with explicit sync and async session APIs.
39
+
40
+ | | |
41
+ |---|---|
42
+ | **PyPI** | [`sqlphilosophy`](https://pypi.org/project/sqlphilosophy/) |
43
+ | **GitHub** | [SignalSafeSoftware/sqlphilosophy](https://github.com/SignalSafeSoftware/sqlphilosophy) |
44
+ | **Import** | `sqlphilosophy` (no root reexports — use explicit submodules below) |
45
+
46
+ Developed in the [DeliveryPlus](https://github.com/SignalSafeSoftware/DeliveryPlus) monorepo under `libs/sqlphilosophy`; this tree is the publishable package source.
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ pip install sqlphilosophy
52
+ ```
53
+
54
+ Async ORM (`AsyncSession`) also needs greenlet:
55
+
56
+ ```bash
57
+ pip install sqlphilosophy[async]
58
+ ```
59
+
60
+ Requires Python 3.12+ and SQLAlchemy 2.x.
61
+
62
+ ## Package layout
63
+
64
+ | Module | Contents |
65
+ |--------|----------|
66
+ | `sqlphilosophy.types` | Portable typing aliases (`RowMapping`, `PrimaryKey`, `SqlFilter`, …) |
67
+ | `sqlphilosophy.sql` | Row mapping helpers, partial updates, Core table helpers, filter builders |
68
+ | `sqlphilosophy.sorting` | `ListQuery`, `SortConfig`, `SortSpec`, pagination/sort resolution |
69
+ | `sqlphilosophy.sync` | Sync `BaseRepository`, `StatementQueryBuilder`, `RepositoryFactory` protocol |
70
+ | `sqlphilosophy.aio` | Async `AsyncBaseRepository`, `AsyncStatementQueryBuilder`, `AsyncRepositoryFactory` |
71
+ | `sqlphilosophy.audit` | Optional SQLAlchemy audit listeners and timestamp mixins |
72
+
73
+ ## Sync usage
74
+
75
+ ```python
76
+ from sqlalchemy.orm import Session
77
+
78
+ from sqlphilosophy.sorting import ListQuery, SortConfig, SortSpec
79
+ from sqlphilosophy.sql import partial_update_model, row_int
80
+ from sqlphilosophy.sync.protocols import RepositoryFactory
81
+ from sqlphilosophy.sync.repository import BaseRepository
82
+ from sqlphilosophy.sync.query import SqlAlchemyStatementBuilder
83
+
84
+ # Without a factory — statement() returns SqlAlchemyStatementBuilder directly
85
+ repo = BaseRepository(User, session)
86
+ rows = repo.statement().where(User.active.is_(True)).mappings().all()
87
+
88
+ # With a factory — statement() and for_repo() delegate to the factory
89
+ repo = BaseRepository(User, session, factory)
90
+ page = repo.statement().fetch_page(ListQuery.from_page(page=1, size=20))
91
+ other = repo.for_repo(OrderRepository)
92
+ ```
93
+
94
+ ## Async usage
95
+
96
+ ```python
97
+ from sqlalchemy.ext.asyncio import AsyncSession
98
+
99
+ from sqlphilosophy.aio.repository import AsyncBaseRepository
100
+
101
+ repo = AsyncBaseRepository(User, session)
102
+ rows = await repo.statement().where(User.active.is_(True)).mappings().all()
103
+ ```
104
+
105
+ ## Audit mixins
106
+
107
+ ```python
108
+ from sqlphilosophy.audit.context import audit_context
109
+ from sqlphilosophy.audit.listener import configure_audit_listeners
110
+ from sqlphilosophy.audit.model import TimestampModel
111
+
112
+ configure_audit_listeners()
113
+
114
+ with audit_context(actor_id=42):
115
+ session.add(MyModel(name="example"))
116
+ session.flush()
117
+ ```
118
+
119
+ ## Development
120
+
121
+ ```bash
122
+ python -m pip install -e ".[dev]"
123
+ python -m pytest
124
+ python -m build
125
+ python -m twine check dist/*
126
+ ```
127
+
128
+ ## Releasing
129
+
130
+ See [RELEASING.md](./RELEASING.md) for GitHub + PyPI trusted publishing.
131
+
132
+ ## License
133
+
134
+ MIT — see [LICENSE](./LICENSE).
@@ -0,0 +1,24 @@
1
+ sqlphilosophy/VERSION,sha256=6d2FB_S_DG9CRY5BrqgzrQvT9hJycjNe7pv01YVB7Wc,6
2
+ sqlphilosophy/__init__.py,sha256=ZyxzTD4IOajt6DKQLYTFAd5FqMZiqARHTqmVatJ8L8I,117
3
+ sqlphilosophy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ sqlphilosophy/sorting.py,sha256=B6FTNj7g08g3MWnoiKpOjKp_b3OLDd4fdVhB48taI3o,3174
5
+ sqlphilosophy/sql.py,sha256=sSxwdR9nybMDdFsCxzMLoDi1bBQtZt-hpUqCSlE3ePw,16605
6
+ sqlphilosophy/types.py,sha256=2ED-HLlymXVUmhjq8sm6Z6MBTxeaELY6f-I2K7mChi0,1754
7
+ sqlphilosophy/aio/__init__.py,sha256=kM_nFV5Gw44MDuBecpXTiYdb0i65wqVC108NA3neQ6s,60
8
+ sqlphilosophy/aio/protocols.py,sha256=4EvHfbahHJO300kUysBKtzl--0_tINliYX0imYGKBo4,901
9
+ sqlphilosophy/aio/query.py,sha256=AkI-SjoGstYB7iVdQYz2We3hdJgnG4l2F0TJvNzF3So,12997
10
+ sqlphilosophy/aio/repository.py,sha256=GbSTFp2EzvHjTrhSml3YL8A-K6Y6Fw4DmnjTXb-lQYM,15664
11
+ sqlphilosophy/audit/__init__.py,sha256=AwN_xJNLVtsc7n_p3Lk485QoVyco4l9pz-K37TOcMnw,70
12
+ sqlphilosophy/audit/context.py,sha256=ckLNvf5g_YqnFui2aDhxXs521DKjLIu1d1GrH3n0h8s,919
13
+ sqlphilosophy/audit/fields.py,sha256=QVf0uC632rFFKzBv1pHoZRLXfq_l7pBfRLWF98zFDyo,625
14
+ sqlphilosophy/audit/listener.py,sha256=0PiCzVXiQKUISZhH2Lhs7KTIb_BD7B1_fs6vGWnnbus,3546
15
+ sqlphilosophy/audit/model.py,sha256=8S9kZdasUzGgV0gUnHnDcEhy4cXTt0NKykghPpETOBk,1839
16
+ sqlphilosophy/sync/__init__.py,sha256=AIxiTARXneNQjb1yEcX_2fI_pCkDMEyVmFPkcVfCig4,59
17
+ sqlphilosophy/sync/protocols.py,sha256=Y8PJmGdeW12H-kfleyA7OvfKnWXypn_KieDalKx6PaE,864
18
+ sqlphilosophy/sync/query.py,sha256=FuuPiLXJfIUyuGqO0D14Adrinqb5sx5f8lw1KN98nuI,12725
19
+ sqlphilosophy/sync/repository.py,sha256=jq8atAGyeuVa1H9bTE6QZODBT6Lj4E-8MeNPrNpYs9w,13792
20
+ sqlphilosophy-0.1.0.dist-info/licenses/LICENSE,sha256=gLXN1zYlPx8WFJGVkSSGrmNEL07Dl7HznSumDZelvio,1063
21
+ sqlphilosophy-0.1.0.dist-info/METADATA,sha256=xq4AhOFxq3yFAmcqnBlcnGk_wUCv1qchZWT8iiAcqTc,4726
22
+ sqlphilosophy-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
+ sqlphilosophy-0.1.0.dist-info/top_level.txt,sha256=sT2j0APcke3yvJP5eQ6PM0kZcePIG-Vf-GR_94aWTcI,14
24
+ sqlphilosophy-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Josh Martin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ sqlphilosophy