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 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
- T_BASEMODEL,
59
- BaseEntity,
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.base", None),
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.base", None),
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": (__SPEC_PARENT__, "persistence.base", None),
244
- "Identifiable": (__SPEC_PARENT__, "persistence.base", None),
245
- "IdentifiableEntity": (__SPEC_PARENT__, "persistence.base", None),
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.base", None),
249
- "DateCriteria": (__SPEC_PARENT__, "persistence.base", None),
250
- "DateOrderedFilter": (__SPEC_PARENT__, "persistence.base", None),
251
- "DateOrderedQueryInjector": (__SPEC_PARENT__, "persistence.base", None),
252
- "Paginated": (__SPEC_PARENT__, "persistence.base", None),
253
- "PaginatedFilter": (__SPEC_PARENT__, "persistence.base", None),
254
- "QueryOperations": (__SPEC_PARENT__, "persistence.base", None),
255
- "CRUDOperations": (__SPEC_PARENT__, "persistence.base", None),
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),
@@ -1,43 +1,9 @@
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
1
+ from typing import Any, Self, Type, TypeVar
20
2
 
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 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,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: jararaca
3
- Version: 0.2.24
3
+ Version: 0.2.25
4
4
  Summary: A simple and fast API framework for Python
5
5
  Home-page: https://github.com/LuscasLeo/jararaca
6
6
  Author: Lucas S
@@ -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=V0xrNYqY4Io-o3Ow1QZda7yc-8GG_qmCAEtrGcJEs8A,1837
4
- jararaca/__init__.py,sha256=fBLWY_hpImDrzD-81avNasZxAbZ8IImAkS3kl83_3Kc,13757
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=rj_BeUiF_AiiLg0dMJcPzcxraukiL6tw3NNzymsg7wI,12594
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.24.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
61
- jararaca-0.2.24.dist-info/METADATA,sha256=gywpebDMPl2EqEG4YJIA2LqBvFDxOZ3l-4Md1Zyxb_k,8552
62
- jararaca-0.2.24.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
63
- jararaca-0.2.24.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
64
- jararaca-0.2.24.dist-info/RECORD,,
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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "jararaca"
3
- version = "0.2.24"
3
+ version = "0.2.25"
4
4
  description = "A simple and fast API framework for Python"
5
5
  authors = ["Lucas S <me@luscasleo.dev>"]
6
6
  readme = "README.md"