jararaca 0.2.24__py3-none-any.whl → 0.2.25__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.
- jararaca/__init__.py +24 -21
- jararaca/persistence/base.py +3 -405
- jararaca/persistence/utilities.py +413 -0
- {jararaca-0.2.24.dist-info → jararaca-0.2.25.dist-info}/METADATA +1 -1
- {jararaca-0.2.24.dist-info → jararaca-0.2.25.dist-info}/RECORD +9 -8
- pyproject.toml +1 -1
- {jararaca-0.2.24.dist-info → jararaca-0.2.25.dist-info}/LICENSE +0 -0
- {jararaca-0.2.24.dist-info → jararaca-0.2.25.dist-info}/WHEEL +0 -0
- {jararaca-0.2.24.dist-info → jararaca-0.2.25.dist-info}/entry_points.txt +0 -0
jararaca/__init__.py
CHANGED
|
@@ -54,9 +54,13 @@ if TYPE_CHECKING:
|
|
|
54
54
|
from .messagebus.types import Message, MessageOf
|
|
55
55
|
from .messagebus.worker import MessageBusWorker
|
|
56
56
|
from .microservice import Microservice, use_app_context, use_current_container
|
|
57
|
-
from .persistence.base import
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
from .persistence.base import T_BASEMODEL, BaseEntity
|
|
58
|
+
from .persistence.interceptors.aiosqa_interceptor import (
|
|
59
|
+
AIOSQAConfig,
|
|
60
|
+
AIOSqlAlchemySessionInterceptor,
|
|
61
|
+
use_session,
|
|
62
|
+
)
|
|
63
|
+
from .persistence.utilities import (
|
|
60
64
|
CriteriaBasedAttributeQueryInjector,
|
|
61
65
|
CRUDOperations,
|
|
62
66
|
DateCriteria,
|
|
@@ -71,11 +75,6 @@ if TYPE_CHECKING:
|
|
|
71
75
|
QueryOperations,
|
|
72
76
|
StringCriteria,
|
|
73
77
|
)
|
|
74
|
-
from .persistence.interceptors.aiosqa_interceptor import (
|
|
75
|
-
AIOSQAConfig,
|
|
76
|
-
AIOSqlAlchemySessionInterceptor,
|
|
77
|
-
use_session,
|
|
78
|
-
)
|
|
79
78
|
from .presentation.decorators import (
|
|
80
79
|
Delete,
|
|
81
80
|
Get,
|
|
@@ -232,27 +231,31 @@ _dynamic_imports: "dict[str, tuple[str, str, str | None]]" = {
|
|
|
232
231
|
"HttpPut": (__SPEC_PARENT__, "rpc.http.decorators", "Put"),
|
|
233
232
|
"HttpDelete": (__SPEC_PARENT__, "rpc.http.decorators", "Delete"),
|
|
234
233
|
"ObservabilityInterceptor": (__SPEC_PARENT__, "observability.interceptor", None),
|
|
235
|
-
"QueryInjector": (__SPEC_PARENT__, "persistence.
|
|
234
|
+
"QueryInjector": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
236
235
|
"HttpMicroservice": (__SPEC_PARENT__, "presentation.http_microservice", None),
|
|
237
236
|
"use_current_container": (__SPEC_PARENT__, "microservice", None),
|
|
238
237
|
"T_BASEMODEL": (__SPEC_PARENT__, "persistence.base", None),
|
|
239
|
-
"DatedEntity": (__SPEC_PARENT__, "persistence.
|
|
238
|
+
"DatedEntity": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
240
239
|
"BaseEntity": (__SPEC_PARENT__, "persistence.base", None),
|
|
241
240
|
"use_ws_manager": (__SPEC_PARENT__, "presentation.websocket.context", None),
|
|
242
241
|
"WebSocketEndpoint": (__SPEC_PARENT__, "presentation.websocket.decorators", None),
|
|
243
|
-
"CriteriaBasedAttributeQueryInjector": (
|
|
244
|
-
|
|
245
|
-
|
|
242
|
+
"CriteriaBasedAttributeQueryInjector": (
|
|
243
|
+
__SPEC_PARENT__,
|
|
244
|
+
"persistence.utilities",
|
|
245
|
+
None,
|
|
246
|
+
),
|
|
247
|
+
"Identifiable": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
248
|
+
"IdentifiableEntity": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
246
249
|
"MessageOf": (__SPEC_PARENT__, "messagebus.types", None),
|
|
247
250
|
"Message": (__SPEC_PARENT__, "messagebus.types", None),
|
|
248
|
-
"StringCriteria": (__SPEC_PARENT__, "persistence.
|
|
249
|
-
"DateCriteria": (__SPEC_PARENT__, "persistence.
|
|
250
|
-
"DateOrderedFilter": (__SPEC_PARENT__, "persistence.
|
|
251
|
-
"DateOrderedQueryInjector": (__SPEC_PARENT__, "persistence.
|
|
252
|
-
"Paginated": (__SPEC_PARENT__, "persistence.
|
|
253
|
-
"PaginatedFilter": (__SPEC_PARENT__, "persistence.
|
|
254
|
-
"QueryOperations": (__SPEC_PARENT__, "persistence.
|
|
255
|
-
"CRUDOperations": (__SPEC_PARENT__, "persistence.
|
|
251
|
+
"StringCriteria": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
252
|
+
"DateCriteria": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
253
|
+
"DateOrderedFilter": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
254
|
+
"DateOrderedQueryInjector": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
255
|
+
"Paginated": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
256
|
+
"PaginatedFilter": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
257
|
+
"QueryOperations": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
258
|
+
"CRUDOperations": (__SPEC_PARENT__, "persistence.utilities", None),
|
|
256
259
|
"RestController": (__SPEC_PARENT__, "presentation.decorators", None),
|
|
257
260
|
"MessageBusController": (__SPEC_PARENT__, "messagebus.decorators", None),
|
|
258
261
|
"MessageHandler": (__SPEC_PARENT__, "messagebus.decorators", None),
|
jararaca/persistence/base.py
CHANGED
|
@@ -1,43 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import logging
|
|
3
|
-
from datetime import UTC, date, datetime
|
|
4
|
-
from functools import reduce
|
|
5
|
-
from typing import (
|
|
6
|
-
Annotated,
|
|
7
|
-
Any,
|
|
8
|
-
Awaitable,
|
|
9
|
-
Callable,
|
|
10
|
-
Generic,
|
|
11
|
-
Iterable,
|
|
12
|
-
Literal,
|
|
13
|
-
Protocol,
|
|
14
|
-
Self,
|
|
15
|
-
Tuple,
|
|
16
|
-
Type,
|
|
17
|
-
TypeVar,
|
|
18
|
-
)
|
|
19
|
-
from uuid import UUID, uuid4
|
|
1
|
+
from typing import Any, Self, Type, TypeVar
|
|
20
2
|
|
|
21
|
-
from pydantic import BaseModel
|
|
22
|
-
from sqlalchemy import
|
|
23
|
-
from sqlalchemy.ext.asyncio import AsyncSession
|
|
24
|
-
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
|
|
25
|
-
|
|
26
|
-
from jararaca.persistence.sort_filter import FilterModel, SortModel
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
27
5
|
|
|
28
6
|
IDENTIFIABLE_SCHEMA_T = TypeVar("IDENTIFIABLE_SCHEMA_T")
|
|
29
|
-
logger = logging.getLogger(__name__)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class Identifiable(BaseModel, Generic[IDENTIFIABLE_SCHEMA_T]):
|
|
33
|
-
id: UUID
|
|
34
|
-
data: IDENTIFIABLE_SCHEMA_T
|
|
35
|
-
|
|
36
|
-
@staticmethod
|
|
37
|
-
def instance(
|
|
38
|
-
id: UUID, data: IDENTIFIABLE_SCHEMA_T
|
|
39
|
-
) -> "Identifiable[IDENTIFIABLE_SCHEMA_T]":
|
|
40
|
-
return Identifiable[IDENTIFIABLE_SCHEMA_T](id=id, data=data)
|
|
41
7
|
|
|
42
8
|
|
|
43
9
|
T_BASEMODEL = TypeVar("T_BASEMODEL", bound=BaseModel)
|
|
@@ -65,371 +31,3 @@ class BaseEntity(DeclarativeBase):
|
|
|
65
31
|
|
|
66
32
|
def to_basemodel(self, model: Type[T_BASEMODEL]) -> T_BASEMODEL:
|
|
67
33
|
return model.model_validate(recursive_get_dict(self))
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def nowutc() -> datetime:
|
|
71
|
-
return datetime.now(UTC)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
class DatedEntity(BaseEntity):
|
|
75
|
-
__abstract__ = True
|
|
76
|
-
|
|
77
|
-
created_at: Mapped[datetime] = mapped_column(
|
|
78
|
-
DateTime(timezone=True), nullable=False, default=nowutc
|
|
79
|
-
)
|
|
80
|
-
updated_at: Mapped[datetime] = mapped_column(
|
|
81
|
-
DateTime(timezone=True), nullable=False, default=nowutc
|
|
82
|
-
)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
class IdentifiableEntity(BaseEntity):
|
|
86
|
-
__abstract__ = True
|
|
87
|
-
|
|
88
|
-
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
89
|
-
|
|
90
|
-
@classmethod
|
|
91
|
-
def from_identifiable(cls, model: Identifiable[T_BASEMODEL]) -> "Self":
|
|
92
|
-
return cls(**{"id": model.id, **model.data.model_dump()})
|
|
93
|
-
|
|
94
|
-
def to_identifiable(self, MODEL: Type[T_BASEMODEL]) -> Identifiable[T_BASEMODEL]:
|
|
95
|
-
try:
|
|
96
|
-
return Identifiable[MODEL].model_validate( # type: ignore[valid-type]
|
|
97
|
-
{"id": self.id, "data": recursive_get_dict(self)}
|
|
98
|
-
)
|
|
99
|
-
except ValidationError:
|
|
100
|
-
logger.critical(
|
|
101
|
-
"Error on to_identifiable for identifiable id %s of class %s table '%s'",
|
|
102
|
-
self.id,
|
|
103
|
-
self.__class__,
|
|
104
|
-
self.__tablename__,
|
|
105
|
-
)
|
|
106
|
-
raise
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
IDENTIFIABLE_T = TypeVar("IDENTIFIABLE_T", bound=IdentifiableEntity)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
class CRUDOperations(Generic[IDENTIFIABLE_T]):
|
|
113
|
-
|
|
114
|
-
def __init__(
|
|
115
|
-
self,
|
|
116
|
-
entity_type: Type[IDENTIFIABLE_T],
|
|
117
|
-
session_provider: Callable[[], AsyncSession],
|
|
118
|
-
) -> None:
|
|
119
|
-
self.entity_type = entity_type
|
|
120
|
-
self.session_provider = session_provider
|
|
121
|
-
|
|
122
|
-
@property
|
|
123
|
-
def session(self) -> AsyncSession:
|
|
124
|
-
return self.session_provider()
|
|
125
|
-
|
|
126
|
-
async def create(self, entity: IDENTIFIABLE_T) -> None:
|
|
127
|
-
self.session.add(entity)
|
|
128
|
-
await self.session.flush()
|
|
129
|
-
await self.session.refresh(entity)
|
|
130
|
-
|
|
131
|
-
async def get(self, id: UUID) -> IDENTIFIABLE_T:
|
|
132
|
-
return await self.session.get_one(self.entity_type, id)
|
|
133
|
-
|
|
134
|
-
async def get_many(self, ids: Iterable[UUID]) -> Iterable[IDENTIFIABLE_T]:
|
|
135
|
-
return await self.session.scalars(
|
|
136
|
-
select(self.entity_type).where(self.entity_type.id.in_(ids))
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
async def update(self, entity: IDENTIFIABLE_T) -> None:
|
|
140
|
-
await self.session.merge(entity)
|
|
141
|
-
|
|
142
|
-
async def delete(self, entity: IDENTIFIABLE_T) -> None:
|
|
143
|
-
await self.session.delete(entity)
|
|
144
|
-
|
|
145
|
-
async def delete_by_id(self, id: UUID) -> None:
|
|
146
|
-
await self.session.execute(
|
|
147
|
-
delete(self.entity_type).where(self.entity_type.id == id)
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
async def update_by_id(self, id: UUID, entity: IDENTIFIABLE_T) -> None:
|
|
151
|
-
await self.session.execute(
|
|
152
|
-
update(self.entity_type)
|
|
153
|
-
.where(self.entity_type.id == id)
|
|
154
|
-
.values(entity.__dict__)
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
async def exists(self, id: UUID) -> bool:
|
|
158
|
-
return (
|
|
159
|
-
await self.session.execute(
|
|
160
|
-
select(
|
|
161
|
-
select(self.entity_type).where(self.entity_type.id == id).exists()
|
|
162
|
-
)
|
|
163
|
-
)
|
|
164
|
-
).scalar_one()
|
|
165
|
-
|
|
166
|
-
async def exists_some(self, ids: Iterable[UUID]) -> bool:
|
|
167
|
-
return (
|
|
168
|
-
await self.session.execute(
|
|
169
|
-
select(
|
|
170
|
-
select(self.entity_type)
|
|
171
|
-
.where(self.entity_type.id.in_(ids))
|
|
172
|
-
.exists()
|
|
173
|
-
)
|
|
174
|
-
)
|
|
175
|
-
).scalar_one()
|
|
176
|
-
|
|
177
|
-
async def exists_all(self, ids: set[UUID]) -> bool:
|
|
178
|
-
|
|
179
|
-
return (
|
|
180
|
-
await self.session.execute(
|
|
181
|
-
select(func.count())
|
|
182
|
-
.select_from(self.entity_type)
|
|
183
|
-
.where(self.entity_type.id.in_(ids))
|
|
184
|
-
)
|
|
185
|
-
).scalar_one() >= len(ids)
|
|
186
|
-
|
|
187
|
-
async def intersects(self, ids: set[UUID]) -> set[UUID]:
|
|
188
|
-
return set(
|
|
189
|
-
(
|
|
190
|
-
await self.session.execute(
|
|
191
|
-
select(self.entity_type.id).where(self.entity_type.id.in_(ids))
|
|
192
|
-
)
|
|
193
|
-
).scalars()
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
async def difference(self, ids: set[UUID]) -> set[UUID]:
|
|
197
|
-
return ids - set(
|
|
198
|
-
(
|
|
199
|
-
await self.session.execute(
|
|
200
|
-
select(self.entity_type.id).where(self.entity_type.id.in_(ids))
|
|
201
|
-
)
|
|
202
|
-
).scalars()
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
# region PaginatedFilter
|
|
207
|
-
class PaginatedFilter(BaseModel):
|
|
208
|
-
page: Annotated[int, Field(gt=-1)] = 1
|
|
209
|
-
page_size: int = 10
|
|
210
|
-
sort_models: list[SortModel] = []
|
|
211
|
-
filter_model: list[FilterModel] = []
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
class QueryInjector(Protocol):
|
|
215
|
-
|
|
216
|
-
def inject(self, query: Select[Tuple[Any]], filter: Any) -> Select[Tuple[Any]]: ...
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# endregion
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
QUERY_ENTITY_T = TypeVar("QUERY_ENTITY_T", bound=BaseEntity)
|
|
223
|
-
QUERY_FILTER_T = TypeVar("QUERY_FILTER_T", bound=PaginatedFilter)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
TRANSFORM_T = TypeVar("TRANSFORM_T")
|
|
227
|
-
PAGINATED_T = TypeVar("PAGINATED_T", bound=Any)
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
class Paginated(BaseModel, Generic[PAGINATED_T]):
|
|
231
|
-
items: list[PAGINATED_T]
|
|
232
|
-
total: int
|
|
233
|
-
unpaginated_total: int
|
|
234
|
-
total_pages: int
|
|
235
|
-
|
|
236
|
-
def transform(
|
|
237
|
-
self,
|
|
238
|
-
transform: Callable[[PAGINATED_T], TRANSFORM_T],
|
|
239
|
-
) -> "Paginated[TRANSFORM_T]":
|
|
240
|
-
return Paginated[TRANSFORM_T](
|
|
241
|
-
items=[transform(item) for item in self.items],
|
|
242
|
-
total=self.total,
|
|
243
|
-
unpaginated_total=self.unpaginated_total,
|
|
244
|
-
total_pages=self.total_pages,
|
|
245
|
-
)
|
|
246
|
-
|
|
247
|
-
async def transform_async(
|
|
248
|
-
self,
|
|
249
|
-
transform: Callable[[PAGINATED_T], Awaitable[TRANSFORM_T]],
|
|
250
|
-
gather: bool = False,
|
|
251
|
-
) -> "Paginated[TRANSFORM_T]":
|
|
252
|
-
"""
|
|
253
|
-
Transform the items of the paginated result asynchronously.
|
|
254
|
-
|
|
255
|
-
Args:
|
|
256
|
-
transform: The transformation function.
|
|
257
|
-
gather: If the items should be gathered in a single async call.
|
|
258
|
-
SQL Alchemy async session queries may cannot be gathered. Use this option with caution.
|
|
259
|
-
"""
|
|
260
|
-
|
|
261
|
-
items = (
|
|
262
|
-
await asyncio.gather(*[transform(item) for item in self.items])
|
|
263
|
-
if gather
|
|
264
|
-
else [await transform(item) for item in self.items]
|
|
265
|
-
)
|
|
266
|
-
return Paginated[TRANSFORM_T](
|
|
267
|
-
items=items,
|
|
268
|
-
total=self.total,
|
|
269
|
-
unpaginated_total=self.unpaginated_total,
|
|
270
|
-
total_pages=self.total_pages,
|
|
271
|
-
)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
275
|
-
|
|
276
|
-
def __init__(
|
|
277
|
-
self,
|
|
278
|
-
entity_type: Type[QUERY_ENTITY_T],
|
|
279
|
-
session_provider: Callable[[], AsyncSession],
|
|
280
|
-
filters_functions: list[QueryInjector],
|
|
281
|
-
unique: bool = False,
|
|
282
|
-
) -> None:
|
|
283
|
-
self.entity_type: type[QUERY_ENTITY_T] = entity_type
|
|
284
|
-
self.session_provider = session_provider
|
|
285
|
-
self.filters_functions = filters_functions
|
|
286
|
-
self.unique = unique
|
|
287
|
-
|
|
288
|
-
@property
|
|
289
|
-
def session(self) -> AsyncSession:
|
|
290
|
-
return self.session_provider()
|
|
291
|
-
|
|
292
|
-
async def query(
|
|
293
|
-
self,
|
|
294
|
-
filter: QUERY_FILTER_T,
|
|
295
|
-
interceptors: list[
|
|
296
|
-
Callable[[Select[Tuple[QUERY_ENTITY_T]]], Select[Tuple[QUERY_ENTITY_T]]]
|
|
297
|
-
] = [],
|
|
298
|
-
) -> "Paginated[QUERY_ENTITY_T]":
|
|
299
|
-
|
|
300
|
-
query = reduce(
|
|
301
|
-
lambda query, interceptor: interceptor(query),
|
|
302
|
-
interceptors,
|
|
303
|
-
select(self.entity_type),
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
unpaginated_total = (
|
|
307
|
-
await self.session.execute(
|
|
308
|
-
select(func.count()).select_from(query.subquery())
|
|
309
|
-
)
|
|
310
|
-
).scalar_one()
|
|
311
|
-
|
|
312
|
-
filtered_query = self.generate_filtered_query(filter, query)
|
|
313
|
-
|
|
314
|
-
paginated_query = filtered_query.limit(filter.page_size).offset(
|
|
315
|
-
(filter.page) * filter.page_size
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
filtered_total = (
|
|
319
|
-
await self.session.execute(
|
|
320
|
-
select(func.count()).select_from(paginated_query.subquery())
|
|
321
|
-
)
|
|
322
|
-
).scalar_one()
|
|
323
|
-
|
|
324
|
-
return Paginated(
|
|
325
|
-
items=[
|
|
326
|
-
e
|
|
327
|
-
for e in self.judge_unique(
|
|
328
|
-
await self.session.execute(paginated_query)
|
|
329
|
-
).scalars()
|
|
330
|
-
],
|
|
331
|
-
total=filtered_total,
|
|
332
|
-
unpaginated_total=unpaginated_total,
|
|
333
|
-
total_pages=int(filtered_total / filter.page_size) + 1,
|
|
334
|
-
)
|
|
335
|
-
|
|
336
|
-
def judge_unique(
|
|
337
|
-
self, result: Result[Tuple[QUERY_ENTITY_T]]
|
|
338
|
-
) -> Result[Tuple[QUERY_ENTITY_T]]:
|
|
339
|
-
if self.unique:
|
|
340
|
-
return result.unique()
|
|
341
|
-
return result
|
|
342
|
-
|
|
343
|
-
def generate_filtered_query(
|
|
344
|
-
self, filter: QUERY_FILTER_T, select_query: Select[Tuple[QUERY_ENTITY_T]]
|
|
345
|
-
) -> Select[Tuple[QUERY_ENTITY_T]]:
|
|
346
|
-
return reduce(
|
|
347
|
-
lambda query, filter_function: filter_function.inject(query, filter),
|
|
348
|
-
self.filters_functions,
|
|
349
|
-
select_query,
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
# DateOrderedFilter
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
class DateOrderedFilter(BaseModel):
|
|
357
|
-
order_by: Literal["asc", "desc"] = "asc"
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
class DateOrderedQueryInjector(QueryInjector):
|
|
361
|
-
|
|
362
|
-
def __init__(self, entity_type: Type[DatedEntity]) -> None:
|
|
363
|
-
self.entity_type = entity_type
|
|
364
|
-
|
|
365
|
-
def inject(
|
|
366
|
-
self,
|
|
367
|
-
query: Select[Tuple[DatedEntity]],
|
|
368
|
-
filter: DateOrderedFilter,
|
|
369
|
-
) -> Select[Tuple[DatedEntity]]:
|
|
370
|
-
return query.order_by(getattr(self.entity_type.created_at, filter.order_by)())
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
# region Criteria
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
# region Criteria
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
class StringCriteria(BaseModel):
|
|
380
|
-
value: str
|
|
381
|
-
is_exact: bool
|
|
382
|
-
case_sensitive: bool
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
class DateCriteria(BaseModel):
|
|
386
|
-
value: date
|
|
387
|
-
op: Literal["eq", "gt", "lt", "gte", "lte"]
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
class DatetimeCriteria(BaseModel):
|
|
391
|
-
value: datetime
|
|
392
|
-
op: Literal["eq", "gt", "lt", "gte", "lte"]
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
class CriteriaBasedAttributeQueryInjector(QueryInjector):
|
|
396
|
-
|
|
397
|
-
def __init__(self, entity_type: Type[BaseEntity]) -> None:
|
|
398
|
-
self.entity_type = entity_type
|
|
399
|
-
|
|
400
|
-
def inject(
|
|
401
|
-
self, query: Select[Tuple[BaseEntity]], filter: Any
|
|
402
|
-
) -> Select[Tuple[BaseEntity]]:
|
|
403
|
-
|
|
404
|
-
attrs = filter.__dict__
|
|
405
|
-
|
|
406
|
-
for field_name, value in attrs.items():
|
|
407
|
-
|
|
408
|
-
if isinstance(value, (DateCriteria, DatetimeCriteria)):
|
|
409
|
-
value = getattr(filter, field_name)
|
|
410
|
-
|
|
411
|
-
entity_field = getattr(self.entity_type, field_name)
|
|
412
|
-
|
|
413
|
-
op_mapping = {
|
|
414
|
-
"eq": entity_field == value.value,
|
|
415
|
-
"gt": entity_field > value.value,
|
|
416
|
-
"lt": entity_field < value.value,
|
|
417
|
-
"gte": entity_field >= value.value,
|
|
418
|
-
"lte": entity_field <= value.value,
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
query = query.filter(op_mapping[value.op])
|
|
422
|
-
elif isinstance(value, StringCriteria):
|
|
423
|
-
value = getattr(filter, field_name)
|
|
424
|
-
|
|
425
|
-
entity_field = getattr(self.entity_type, field_name)
|
|
426
|
-
|
|
427
|
-
if value.is_exact:
|
|
428
|
-
query = query.filter(entity_field == value.value)
|
|
429
|
-
else:
|
|
430
|
-
query = query.filter(entity_field.contains(value.value))
|
|
431
|
-
|
|
432
|
-
return query
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
# endregion
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from datetime import UTC, date, datetime
|
|
4
|
+
from functools import reduce
|
|
5
|
+
from typing import (
|
|
6
|
+
Annotated,
|
|
7
|
+
Any,
|
|
8
|
+
Awaitable,
|
|
9
|
+
Callable,
|
|
10
|
+
Generic,
|
|
11
|
+
Iterable,
|
|
12
|
+
Literal,
|
|
13
|
+
Protocol,
|
|
14
|
+
Self,
|
|
15
|
+
Tuple,
|
|
16
|
+
Type,
|
|
17
|
+
TypeVar,
|
|
18
|
+
)
|
|
19
|
+
from uuid import UUID, uuid4
|
|
20
|
+
|
|
21
|
+
from pydantic import BaseModel, Field, ValidationError
|
|
22
|
+
from sqlalchemy import DateTime, Result, Select, delete, func, select, update
|
|
23
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
24
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
25
|
+
|
|
26
|
+
from jararaca.persistence.base import (
|
|
27
|
+
IDENTIFIABLE_SCHEMA_T,
|
|
28
|
+
T_BASEMODEL,
|
|
29
|
+
BaseEntity,
|
|
30
|
+
recursive_get_dict,
|
|
31
|
+
)
|
|
32
|
+
from jararaca.persistence.sort_filter import FilterModel, SortModel
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Identifiable(BaseModel, Generic[IDENTIFIABLE_SCHEMA_T]):
|
|
38
|
+
id: UUID
|
|
39
|
+
data: IDENTIFIABLE_SCHEMA_T
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def instance(
|
|
43
|
+
id: UUID, data: IDENTIFIABLE_SCHEMA_T
|
|
44
|
+
) -> "Identifiable[IDENTIFIABLE_SCHEMA_T]":
|
|
45
|
+
return Identifiable[IDENTIFIABLE_SCHEMA_T](id=id, data=data)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def nowutc() -> datetime:
|
|
49
|
+
return datetime.now(UTC)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DatedEntity(BaseEntity):
|
|
53
|
+
__abstract__ = True
|
|
54
|
+
|
|
55
|
+
created_at: Mapped[datetime] = mapped_column(
|
|
56
|
+
DateTime(timezone=True), nullable=False, default=nowutc
|
|
57
|
+
)
|
|
58
|
+
updated_at: Mapped[datetime] = mapped_column(
|
|
59
|
+
DateTime(timezone=True), nullable=False, default=nowutc
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class IdentifiableEntity(BaseEntity):
|
|
64
|
+
__abstract__ = True
|
|
65
|
+
|
|
66
|
+
id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def from_identifiable(cls, model: Identifiable[T_BASEMODEL]) -> "Self":
|
|
70
|
+
return cls(**{"id": model.id, **model.data.model_dump()})
|
|
71
|
+
|
|
72
|
+
def to_identifiable(self, MODEL: Type[T_BASEMODEL]) -> Identifiable[T_BASEMODEL]:
|
|
73
|
+
try:
|
|
74
|
+
return Identifiable[MODEL].model_validate( # type: ignore[valid-type]
|
|
75
|
+
{"id": self.id, "data": recursive_get_dict(self)}
|
|
76
|
+
)
|
|
77
|
+
except ValidationError:
|
|
78
|
+
logger.critical(
|
|
79
|
+
"Error on to_identifiable for identifiable id %s of class %s table '%s'",
|
|
80
|
+
self.id,
|
|
81
|
+
self.__class__,
|
|
82
|
+
self.__tablename__,
|
|
83
|
+
)
|
|
84
|
+
raise
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
IDENTIFIABLE_T = TypeVar("IDENTIFIABLE_T", bound=IdentifiableEntity)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class CRUDOperations(Generic[IDENTIFIABLE_T]):
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
entity_type: Type[IDENTIFIABLE_T],
|
|
95
|
+
session_provider: Callable[[], AsyncSession],
|
|
96
|
+
) -> None:
|
|
97
|
+
self.entity_type = entity_type
|
|
98
|
+
self.session_provider = session_provider
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def session(self) -> AsyncSession:
|
|
102
|
+
return self.session_provider()
|
|
103
|
+
|
|
104
|
+
async def create(self, entity: IDENTIFIABLE_T) -> None:
|
|
105
|
+
self.session.add(entity)
|
|
106
|
+
await self.session.flush()
|
|
107
|
+
await self.session.refresh(entity)
|
|
108
|
+
|
|
109
|
+
async def get(self, id: UUID) -> IDENTIFIABLE_T:
|
|
110
|
+
return await self.session.get_one(self.entity_type, id)
|
|
111
|
+
|
|
112
|
+
async def get_many(self, ids: Iterable[UUID]) -> Iterable[IDENTIFIABLE_T]:
|
|
113
|
+
return await self.session.scalars(
|
|
114
|
+
select(self.entity_type).where(self.entity_type.id.in_(ids))
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def update(self, entity: IDENTIFIABLE_T) -> None:
|
|
118
|
+
await self.session.merge(entity)
|
|
119
|
+
|
|
120
|
+
async def delete(self, entity: IDENTIFIABLE_T) -> None:
|
|
121
|
+
await self.session.delete(entity)
|
|
122
|
+
|
|
123
|
+
async def delete_by_id(self, id: UUID) -> None:
|
|
124
|
+
await self.session.execute(
|
|
125
|
+
delete(self.entity_type).where(self.entity_type.id == id)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
async def update_by_id(self, id: UUID, entity: IDENTIFIABLE_T) -> None:
|
|
129
|
+
await self.session.execute(
|
|
130
|
+
update(self.entity_type)
|
|
131
|
+
.where(self.entity_type.id == id)
|
|
132
|
+
.values(entity.__dict__)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def exists(self, id: UUID) -> bool:
|
|
136
|
+
return (
|
|
137
|
+
await self.session.execute(
|
|
138
|
+
select(
|
|
139
|
+
select(self.entity_type).where(self.entity_type.id == id).exists()
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
).scalar_one()
|
|
143
|
+
|
|
144
|
+
async def exists_some(self, ids: Iterable[UUID]) -> bool:
|
|
145
|
+
return (
|
|
146
|
+
await self.session.execute(
|
|
147
|
+
select(
|
|
148
|
+
select(self.entity_type)
|
|
149
|
+
.where(self.entity_type.id.in_(ids))
|
|
150
|
+
.exists()
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
).scalar_one()
|
|
154
|
+
|
|
155
|
+
async def exists_all(self, ids: set[UUID]) -> bool:
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
await self.session.execute(
|
|
159
|
+
select(func.count())
|
|
160
|
+
.select_from(self.entity_type)
|
|
161
|
+
.where(self.entity_type.id.in_(ids))
|
|
162
|
+
)
|
|
163
|
+
).scalar_one() >= len(ids)
|
|
164
|
+
|
|
165
|
+
async def intersects(self, ids: set[UUID]) -> set[UUID]:
|
|
166
|
+
return set(
|
|
167
|
+
(
|
|
168
|
+
await self.session.execute(
|
|
169
|
+
select(self.entity_type.id).where(self.entity_type.id.in_(ids))
|
|
170
|
+
)
|
|
171
|
+
).scalars()
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
async def difference(self, ids: set[UUID]) -> set[UUID]:
|
|
175
|
+
return ids - set(
|
|
176
|
+
(
|
|
177
|
+
await self.session.execute(
|
|
178
|
+
select(self.entity_type.id).where(self.entity_type.id.in_(ids))
|
|
179
|
+
)
|
|
180
|
+
).scalars()
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
# region PaginatedFilter
|
|
185
|
+
class PaginatedFilter(BaseModel):
|
|
186
|
+
page: Annotated[int, Field(gt=-1)] = 1
|
|
187
|
+
page_size: int = 10
|
|
188
|
+
sort_models: list[SortModel] = []
|
|
189
|
+
filter_model: list[FilterModel] = []
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class QueryInjector(Protocol):
|
|
193
|
+
|
|
194
|
+
def inject(self, query: Select[Tuple[Any]], filter: Any) -> Select[Tuple[Any]]: ...
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# endregion
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
QUERY_ENTITY_T = TypeVar("QUERY_ENTITY_T", bound=BaseEntity)
|
|
201
|
+
QUERY_FILTER_T = TypeVar("QUERY_FILTER_T", bound=PaginatedFilter)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
TRANSFORM_T = TypeVar("TRANSFORM_T")
|
|
205
|
+
PAGINATED_T = TypeVar("PAGINATED_T", bound=Any)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class Paginated(BaseModel, Generic[PAGINATED_T]):
|
|
209
|
+
items: list[PAGINATED_T]
|
|
210
|
+
total: int
|
|
211
|
+
unpaginated_total: int
|
|
212
|
+
total_pages: int
|
|
213
|
+
|
|
214
|
+
def transform(
|
|
215
|
+
self,
|
|
216
|
+
transform: Callable[[PAGINATED_T], TRANSFORM_T],
|
|
217
|
+
) -> "Paginated[TRANSFORM_T]":
|
|
218
|
+
return Paginated[TRANSFORM_T](
|
|
219
|
+
items=[transform(item) for item in self.items],
|
|
220
|
+
total=self.total,
|
|
221
|
+
unpaginated_total=self.unpaginated_total,
|
|
222
|
+
total_pages=self.total_pages,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
async def transform_async(
|
|
226
|
+
self,
|
|
227
|
+
transform: Callable[[PAGINATED_T], Awaitable[TRANSFORM_T]],
|
|
228
|
+
gather: bool = False,
|
|
229
|
+
) -> "Paginated[TRANSFORM_T]":
|
|
230
|
+
"""
|
|
231
|
+
Transform the items of the paginated result asynchronously.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
transform: The transformation function.
|
|
235
|
+
gather: If the items should be gathered in a single async call.
|
|
236
|
+
SQL Alchemy async session queries may cannot be gathered. Use this option with caution.
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
items = (
|
|
240
|
+
await asyncio.gather(*[transform(item) for item in self.items])
|
|
241
|
+
if gather
|
|
242
|
+
else [await transform(item) for item in self.items]
|
|
243
|
+
)
|
|
244
|
+
return Paginated[TRANSFORM_T](
|
|
245
|
+
items=items,
|
|
246
|
+
total=self.total,
|
|
247
|
+
unpaginated_total=self.unpaginated_total,
|
|
248
|
+
total_pages=self.total_pages,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
|
|
253
|
+
|
|
254
|
+
def __init__(
|
|
255
|
+
self,
|
|
256
|
+
entity_type: Type[QUERY_ENTITY_T],
|
|
257
|
+
session_provider: Callable[[], AsyncSession],
|
|
258
|
+
filters_functions: list[QueryInjector],
|
|
259
|
+
unique: bool = False,
|
|
260
|
+
) -> None:
|
|
261
|
+
self.entity_type: type[QUERY_ENTITY_T] = entity_type
|
|
262
|
+
self.session_provider = session_provider
|
|
263
|
+
self.filters_functions = filters_functions
|
|
264
|
+
self.unique = unique
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def session(self) -> AsyncSession:
|
|
268
|
+
return self.session_provider()
|
|
269
|
+
|
|
270
|
+
async def query(
|
|
271
|
+
self,
|
|
272
|
+
filter: QUERY_FILTER_T,
|
|
273
|
+
interceptors: list[
|
|
274
|
+
Callable[[Select[Tuple[QUERY_ENTITY_T]]], Select[Tuple[QUERY_ENTITY_T]]]
|
|
275
|
+
] = [],
|
|
276
|
+
) -> "Paginated[QUERY_ENTITY_T]":
|
|
277
|
+
|
|
278
|
+
query = reduce(
|
|
279
|
+
lambda query, interceptor: interceptor(query),
|
|
280
|
+
interceptors,
|
|
281
|
+
select(self.entity_type),
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
unpaginated_total = (
|
|
285
|
+
await self.session.execute(
|
|
286
|
+
select(func.count()).select_from(query.subquery())
|
|
287
|
+
)
|
|
288
|
+
).scalar_one()
|
|
289
|
+
|
|
290
|
+
filtered_query = self.generate_filtered_query(filter, query)
|
|
291
|
+
|
|
292
|
+
paginated_query = filtered_query.limit(filter.page_size).offset(
|
|
293
|
+
(filter.page) * filter.page_size
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
filtered_total = (
|
|
297
|
+
await self.session.execute(
|
|
298
|
+
select(func.count()).select_from(paginated_query.subquery())
|
|
299
|
+
)
|
|
300
|
+
).scalar_one()
|
|
301
|
+
|
|
302
|
+
return Paginated(
|
|
303
|
+
items=[
|
|
304
|
+
e
|
|
305
|
+
for e in self.judge_unique(
|
|
306
|
+
await self.session.execute(paginated_query)
|
|
307
|
+
).scalars()
|
|
308
|
+
],
|
|
309
|
+
total=filtered_total,
|
|
310
|
+
unpaginated_total=unpaginated_total,
|
|
311
|
+
total_pages=int(filtered_total / filter.page_size) + 1,
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def judge_unique(
|
|
315
|
+
self, result: Result[Tuple[QUERY_ENTITY_T]]
|
|
316
|
+
) -> Result[Tuple[QUERY_ENTITY_T]]:
|
|
317
|
+
if self.unique:
|
|
318
|
+
return result.unique()
|
|
319
|
+
return result
|
|
320
|
+
|
|
321
|
+
def generate_filtered_query(
|
|
322
|
+
self, filter: QUERY_FILTER_T, select_query: Select[Tuple[QUERY_ENTITY_T]]
|
|
323
|
+
) -> Select[Tuple[QUERY_ENTITY_T]]:
|
|
324
|
+
return reduce(
|
|
325
|
+
lambda query, filter_function: filter_function.inject(query, filter),
|
|
326
|
+
self.filters_functions,
|
|
327
|
+
select_query,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# DateOrderedFilter
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class DateOrderedFilter(BaseModel):
|
|
335
|
+
order_by: Literal["asc", "desc"] = "asc"
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class DateOrderedQueryInjector(QueryInjector):
|
|
339
|
+
|
|
340
|
+
def __init__(self, entity_type: Type[DatedEntity]) -> None:
|
|
341
|
+
self.entity_type = entity_type
|
|
342
|
+
|
|
343
|
+
def inject(
|
|
344
|
+
self,
|
|
345
|
+
query: Select[Tuple[DatedEntity]],
|
|
346
|
+
filter: DateOrderedFilter,
|
|
347
|
+
) -> Select[Tuple[DatedEntity]]:
|
|
348
|
+
return query.order_by(getattr(self.entity_type.created_at, filter.order_by)())
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# region Criteria
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
# region Criteria
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class StringCriteria(BaseModel):
|
|
358
|
+
value: str
|
|
359
|
+
is_exact: bool
|
|
360
|
+
case_sensitive: bool
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class DateCriteria(BaseModel):
|
|
364
|
+
value: date
|
|
365
|
+
op: Literal["eq", "gt", "lt", "gte", "lte"]
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
class DatetimeCriteria(BaseModel):
|
|
369
|
+
value: datetime
|
|
370
|
+
op: Literal["eq", "gt", "lt", "gte", "lte"]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class CriteriaBasedAttributeQueryInjector(QueryInjector):
|
|
374
|
+
|
|
375
|
+
def __init__(self, entity_type: Type[BaseEntity]) -> None:
|
|
376
|
+
self.entity_type = entity_type
|
|
377
|
+
|
|
378
|
+
def inject(
|
|
379
|
+
self, query: Select[Tuple[BaseEntity]], filter: Any
|
|
380
|
+
) -> Select[Tuple[BaseEntity]]:
|
|
381
|
+
|
|
382
|
+
attrs = filter.__dict__
|
|
383
|
+
|
|
384
|
+
for field_name, value in attrs.items():
|
|
385
|
+
|
|
386
|
+
if isinstance(value, (DateCriteria, DatetimeCriteria)):
|
|
387
|
+
value = getattr(filter, field_name)
|
|
388
|
+
|
|
389
|
+
entity_field = getattr(self.entity_type, field_name)
|
|
390
|
+
|
|
391
|
+
op_mapping = {
|
|
392
|
+
"eq": entity_field == value.value,
|
|
393
|
+
"gt": entity_field > value.value,
|
|
394
|
+
"lt": entity_field < value.value,
|
|
395
|
+
"gte": entity_field >= value.value,
|
|
396
|
+
"lte": entity_field <= value.value,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
query = query.filter(op_mapping[value.op])
|
|
400
|
+
elif isinstance(value, StringCriteria):
|
|
401
|
+
value = getattr(filter, field_name)
|
|
402
|
+
|
|
403
|
+
entity_field = getattr(self.entity_type, field_name)
|
|
404
|
+
|
|
405
|
+
if value.is_exact:
|
|
406
|
+
query = query.filter(entity_field == value.value)
|
|
407
|
+
else:
|
|
408
|
+
query = query.filter(entity_field.contains(value.value))
|
|
409
|
+
|
|
410
|
+
return query
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
# endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
2
2
|
README.md,sha256=mte30I-ZEJJp-Oax-OganNgl6G9GaCZPL6JVFAvZGz4,7034
|
|
3
|
-
pyproject.toml,sha256=
|
|
4
|
-
jararaca/__init__.py,sha256=
|
|
3
|
+
pyproject.toml,sha256=3xtKWnGmMTaIitU2R8vmQrS48cCtJ4ci0T5rqb07ymE,1837
|
|
4
|
+
jararaca/__init__.py,sha256=h1lYQMmB8TATPG0GXjIzLZWHbWFhvkAFu-yBjm2W18g,13875
|
|
5
5
|
jararaca/__main__.py,sha256=-O3vsB5lHdqNFjUtoELDF81IYFtR-DSiiFMzRaiSsv4,67
|
|
6
6
|
jararaca/cli.py,sha256=fh7lp7rf5xbV5VaoSYWWehktel6BPcOXMjW7cw4wKms,5693
|
|
7
7
|
jararaca/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -23,12 +23,13 @@ jararaca/observability/decorators.py,sha256=XffBinFXdiNkY6eo8_1nkr_GapM0RUGBg0ai
|
|
|
23
23
|
jararaca/observability/interceptor.py,sha256=GHkuGKFWftN7MDjvYeGFGEPnuJETNhtxRK6yuPrCrpU,1462
|
|
24
24
|
jararaca/observability/providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
25
25
|
jararaca/observability/providers/otel.py,sha256=LgfoITdoQTCxKebfLcEfwMiG992wlWY_0AUTd2fo8hY,6065
|
|
26
|
-
jararaca/persistence/base.py,sha256=
|
|
26
|
+
jararaca/persistence/base.py,sha256=Xfnpvj3yeLdpVBifH5W6AwPCLwL2ot0dpLzbPg1zwkQ,966
|
|
27
27
|
jararaca/persistence/exports.py,sha256=Ghx4yoFaB4QVTb9WxrFYgmcSATXMNvrOvT8ybPNKXCA,62
|
|
28
28
|
jararaca/persistence/interceptors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
29
29
|
jararaca/persistence/interceptors/aiosqa_interceptor.py,sha256=H6ZjOdosYGCZUzKjugiXQwJkAbnsL4HnkZLOEQhULEc,1986
|
|
30
30
|
jararaca/persistence/session.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
31
31
|
jararaca/persistence/sort_filter.py,sha256=bUxUkFZ_W1Vuc0TyDzhA41P7Zjb-USWzcTD9kAimRq4,6806
|
|
32
|
+
jararaca/persistence/utilities.py,sha256=nZA_CKk_qyZ0GhwHrRmTGyN07z0cyU8wrPTyphDtfSM,11857
|
|
32
33
|
jararaca/presentation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
34
|
jararaca/presentation/decorators.py,sha256=eL2YCgMSr19m4YCri5PQU46NRxf0QxsqDnz6MqKu0YQ,8389
|
|
34
35
|
jararaca/presentation/hooks.py,sha256=WBbU5DG3-MAm2Ro2YraQyYG_HENfizYfyShL2ktHi6k,1980
|
|
@@ -57,8 +58,8 @@ jararaca/tools/app_config/decorators.py,sha256=-ckkMZ1dswOmECdo1rFrZ15UAku--txaN
|
|
|
57
58
|
jararaca/tools/app_config/interceptor.py,sha256=nfFZiS80hrbnL7-XEYrwmp2rwaVYBqxvqu3Y-6o_ov4,2575
|
|
58
59
|
jararaca/tools/metadata.py,sha256=7nlCDYgItNybentPSSCc2MLqN7IpBd0VyQzfjfQycVI,1402
|
|
59
60
|
jararaca/tools/typescript/interface_parser.py,sha256=rvTlSGDffyxSwqoHDLxdXApwXDw0v8Tq6nOWPO033nQ,28382
|
|
60
|
-
jararaca-0.2.
|
|
61
|
-
jararaca-0.2.
|
|
62
|
-
jararaca-0.2.
|
|
63
|
-
jararaca-0.2.
|
|
64
|
-
jararaca-0.2.
|
|
61
|
+
jararaca-0.2.25.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
62
|
+
jararaca-0.2.25.dist-info/METADATA,sha256=psi20F1fXMEO4ATevCDQt8dPS2tuVjTSR4PRN0vO8SA,8552
|
|
63
|
+
jararaca-0.2.25.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
64
|
+
jararaca-0.2.25.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
|
|
65
|
+
jararaca-0.2.25.dist-info/RECORD,,
|
pyproject.toml
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|