jararaca 0.2.23__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,41 +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
3
+ from pydantic import BaseModel
4
+ from sqlalchemy.orm import DeclarativeBase
25
5
 
26
6
  IDENTIFIABLE_SCHEMA_T = TypeVar("IDENTIFIABLE_SCHEMA_T")
27
- logger = logging.getLogger(__name__)
28
-
29
-
30
- class Identifiable(BaseModel, Generic[IDENTIFIABLE_SCHEMA_T]):
31
- id: UUID
32
- data: IDENTIFIABLE_SCHEMA_T
33
-
34
- @staticmethod
35
- def instance(
36
- id: UUID, data: IDENTIFIABLE_SCHEMA_T
37
- ) -> "Identifiable[IDENTIFIABLE_SCHEMA_T]":
38
- return Identifiable[IDENTIFIABLE_SCHEMA_T](id=id, data=data)
39
7
 
40
8
 
41
9
  T_BASEMODEL = TypeVar("T_BASEMODEL", bound=BaseModel)
@@ -63,369 +31,3 @@ class BaseEntity(DeclarativeBase):
63
31
 
64
32
  def to_basemodel(self, model: Type[T_BASEMODEL]) -> T_BASEMODEL:
65
33
  return model.model_validate(recursive_get_dict(self))
66
-
67
-
68
- def nowutc() -> datetime:
69
- return datetime.now(UTC)
70
-
71
-
72
- class DatedEntity(BaseEntity):
73
- __abstract__ = True
74
-
75
- created_at: Mapped[datetime] = mapped_column(
76
- DateTime(timezone=True), nullable=False, default=nowutc
77
- )
78
- updated_at: Mapped[datetime] = mapped_column(
79
- DateTime(timezone=True), nullable=False, default=nowutc
80
- )
81
-
82
-
83
- class IdentifiableEntity(BaseEntity):
84
- __abstract__ = True
85
-
86
- id: Mapped[UUID] = mapped_column(primary_key=True, default=uuid4)
87
-
88
- @classmethod
89
- def from_identifiable(cls, model: Identifiable[T_BASEMODEL]) -> "Self":
90
- return cls(**{"id": model.id, **model.data.model_dump()})
91
-
92
- def to_identifiable(self, MODEL: Type[T_BASEMODEL]) -> Identifiable[T_BASEMODEL]:
93
- try:
94
- return Identifiable[MODEL].model_validate( # type: ignore[valid-type]
95
- {"id": self.id, "data": recursive_get_dict(self)}
96
- )
97
- except ValidationError:
98
- logger.critical(
99
- "Error on to_identifiable for identifiable id %s of class %s table '%s'",
100
- self.id,
101
- self.__class__,
102
- self.__tablename__,
103
- )
104
- raise
105
-
106
-
107
- IDENTIFIABLE_T = TypeVar("IDENTIFIABLE_T", bound=IdentifiableEntity)
108
-
109
-
110
- class CRUDOperations(Generic[IDENTIFIABLE_T]):
111
-
112
- def __init__(
113
- self,
114
- entity_type: Type[IDENTIFIABLE_T],
115
- session_provider: Callable[[], AsyncSession],
116
- ) -> None:
117
- self.entity_type = entity_type
118
- self.session_provider = session_provider
119
-
120
- @property
121
- def session(self) -> AsyncSession:
122
- return self.session_provider()
123
-
124
- async def create(self, entity: IDENTIFIABLE_T) -> None:
125
- self.session.add(entity)
126
- await self.session.flush()
127
- await self.session.refresh(entity)
128
-
129
- async def get(self, id: UUID) -> IDENTIFIABLE_T:
130
- return await self.session.get_one(self.entity_type, id)
131
-
132
- async def get_many(self, ids: Iterable[UUID]) -> Iterable[IDENTIFIABLE_T]:
133
- return await self.session.scalars(
134
- select(self.entity_type).where(self.entity_type.id.in_(ids))
135
- )
136
-
137
- async def update(self, entity: IDENTIFIABLE_T) -> None:
138
- await self.session.merge(entity)
139
-
140
- async def delete(self, entity: IDENTIFIABLE_T) -> None:
141
- await self.session.delete(entity)
142
-
143
- async def delete_by_id(self, id: UUID) -> None:
144
- await self.session.execute(
145
- delete(self.entity_type).where(self.entity_type.id == id)
146
- )
147
-
148
- async def update_by_id(self, id: UUID, entity: IDENTIFIABLE_T) -> None:
149
- await self.session.execute(
150
- update(self.entity_type)
151
- .where(self.entity_type.id == id)
152
- .values(entity.__dict__)
153
- )
154
-
155
- async def exists(self, id: UUID) -> bool:
156
- return (
157
- await self.session.execute(
158
- select(
159
- select(self.entity_type).where(self.entity_type.id == id).exists()
160
- )
161
- )
162
- ).scalar_one()
163
-
164
- async def exists_some(self, ids: Iterable[UUID]) -> bool:
165
- return (
166
- await self.session.execute(
167
- select(
168
- select(self.entity_type)
169
- .where(self.entity_type.id.in_(ids))
170
- .exists()
171
- )
172
- )
173
- ).scalar_one()
174
-
175
- async def exists_all(self, ids: set[UUID]) -> bool:
176
-
177
- return (
178
- await self.session.execute(
179
- select(func.count())
180
- .select_from(self.entity_type)
181
- .where(self.entity_type.id.in_(ids))
182
- )
183
- ).scalar_one() >= len(ids)
184
-
185
- async def intersects(self, ids: set[UUID]) -> set[UUID]:
186
- return set(
187
- (
188
- await self.session.execute(
189
- select(self.entity_type.id).where(self.entity_type.id.in_(ids))
190
- )
191
- ).scalars()
192
- )
193
-
194
- async def difference(self, ids: set[UUID]) -> set[UUID]:
195
- return ids - set(
196
- (
197
- await self.session.execute(
198
- select(self.entity_type.id).where(self.entity_type.id.in_(ids))
199
- )
200
- ).scalars()
201
- )
202
-
203
-
204
- # region PaginatedFilter
205
- class PaginatedFilter(BaseModel):
206
- page: Annotated[int, Field(gt=-1)] = 1
207
- page_size: int = 10
208
-
209
-
210
- class QueryInjector(Protocol):
211
-
212
- def inject(self, query: Select[Tuple[Any]], filter: Any) -> Select[Tuple[Any]]: ...
213
-
214
-
215
- # endregion
216
-
217
-
218
- QUERY_ENTITY_T = TypeVar("QUERY_ENTITY_T", bound=BaseEntity)
219
- QUERY_FILTER_T = TypeVar("QUERY_FILTER_T", bound=PaginatedFilter)
220
-
221
-
222
- TRANSFORM_T = TypeVar("TRANSFORM_T")
223
- PAGINATED_T = TypeVar("PAGINATED_T", bound=Any)
224
-
225
-
226
- class Paginated(BaseModel, Generic[PAGINATED_T]):
227
- items: list[PAGINATED_T]
228
- total: int
229
- unpaginated_total: int
230
- total_pages: int
231
-
232
- def transform(
233
- self,
234
- transform: Callable[[PAGINATED_T], TRANSFORM_T],
235
- ) -> "Paginated[TRANSFORM_T]":
236
- return Paginated[TRANSFORM_T](
237
- items=[transform(item) for item in self.items],
238
- total=self.total,
239
- unpaginated_total=self.unpaginated_total,
240
- total_pages=self.total_pages,
241
- )
242
-
243
- async def transform_async(
244
- self,
245
- transform: Callable[[PAGINATED_T], Awaitable[TRANSFORM_T]],
246
- gather: bool = False,
247
- ) -> "Paginated[TRANSFORM_T]":
248
- """
249
- Transform the items of the paginated result asynchronously.
250
-
251
- Args:
252
- transform: The transformation function.
253
- gather: If the items should be gathered in a single async call.
254
- SQL Alchemy async session queries may cannot be gathered. Use this option with caution.
255
- """
256
-
257
- items = (
258
- await asyncio.gather(*[transform(item) for item in self.items])
259
- if gather
260
- else [await transform(item) for item in self.items]
261
- )
262
- return Paginated[TRANSFORM_T](
263
- items=items,
264
- total=self.total,
265
- unpaginated_total=self.unpaginated_total,
266
- total_pages=self.total_pages,
267
- )
268
-
269
-
270
- class QueryOperations(Generic[QUERY_FILTER_T, QUERY_ENTITY_T]):
271
-
272
- def __init__(
273
- self,
274
- entity_type: Type[QUERY_ENTITY_T],
275
- session_provider: Callable[[], AsyncSession],
276
- filters_functions: list[QueryInjector],
277
- unique: bool = False,
278
- ) -> None:
279
- self.entity_type: type[QUERY_ENTITY_T] = entity_type
280
- self.session_provider = session_provider
281
- self.filters_functions = filters_functions
282
- self.unique = unique
283
-
284
- @property
285
- def session(self) -> AsyncSession:
286
- return self.session_provider()
287
-
288
- async def query(
289
- self,
290
- filter: QUERY_FILTER_T,
291
- interceptors: list[
292
- Callable[[Select[Tuple[QUERY_ENTITY_T]]], Select[Tuple[QUERY_ENTITY_T]]]
293
- ] = [],
294
- ) -> "Paginated[QUERY_ENTITY_T]":
295
-
296
- query = reduce(
297
- lambda query, interceptor: interceptor(query),
298
- interceptors,
299
- select(self.entity_type),
300
- )
301
-
302
- unpaginated_total = (
303
- await self.session.execute(
304
- select(func.count()).select_from(query.subquery())
305
- )
306
- ).scalar_one()
307
-
308
- filtered_query = self.generate_filtered_query(filter, query)
309
-
310
- paginated_query = filtered_query.limit(filter.page_size).offset(
311
- (filter.page) * filter.page_size
312
- )
313
-
314
- filtered_total = (
315
- await self.session.execute(
316
- select(func.count()).select_from(paginated_query.subquery())
317
- )
318
- ).scalar_one()
319
-
320
- return Paginated(
321
- items=[
322
- e
323
- for e in self.judge_unique(
324
- await self.session.execute(paginated_query)
325
- ).scalars()
326
- ],
327
- total=filtered_total,
328
- unpaginated_total=unpaginated_total,
329
- total_pages=int(filtered_total / filter.page_size) + 1,
330
- )
331
-
332
- def judge_unique(
333
- self, result: Result[Tuple[QUERY_ENTITY_T]]
334
- ) -> Result[Tuple[QUERY_ENTITY_T]]:
335
- if self.unique:
336
- return result.unique()
337
- return result
338
-
339
- def generate_filtered_query(
340
- self, filter: QUERY_FILTER_T, select_query: Select[Tuple[QUERY_ENTITY_T]]
341
- ) -> Select[Tuple[QUERY_ENTITY_T]]:
342
- return reduce(
343
- lambda query, filter_function: filter_function.inject(query, filter),
344
- self.filters_functions,
345
- select_query,
346
- )
347
-
348
-
349
- # DateOrderedFilter
350
-
351
-
352
- class DateOrderedFilter(BaseModel):
353
- order_by: Literal["asc", "desc"] = "asc"
354
-
355
-
356
- class DateOrderedQueryInjector(QueryInjector):
357
-
358
- def __init__(self, entity_type: Type[DatedEntity]) -> None:
359
- self.entity_type = entity_type
360
-
361
- def inject(
362
- self,
363
- query: Select[Tuple[DatedEntity]],
364
- filter: DateOrderedFilter,
365
- ) -> Select[Tuple[DatedEntity]]:
366
- return query.order_by(getattr(self.entity_type.created_at, filter.order_by)())
367
-
368
-
369
- # region Criteria
370
-
371
-
372
- # region Criteria
373
-
374
-
375
- class StringCriteria(BaseModel):
376
- value: str
377
- is_exact: bool
378
- case_sensitive: bool
379
-
380
-
381
- class DateCriteria(BaseModel):
382
- value: date
383
- op: Literal["eq", "gt", "lt", "gte", "lte"]
384
-
385
-
386
- class DatetimeCriteria(BaseModel):
387
- value: datetime
388
- op: Literal["eq", "gt", "lt", "gte", "lte"]
389
-
390
-
391
- class CriteriaBasedAttributeQueryInjector(QueryInjector):
392
-
393
- def __init__(self, entity_type: Type[BaseEntity]) -> None:
394
- self.entity_type = entity_type
395
-
396
- def inject(
397
- self, query: Select[Tuple[BaseEntity]], filter: Any
398
- ) -> Select[Tuple[BaseEntity]]:
399
-
400
- attrs = filter.__dict__
401
-
402
- for field_name, value in attrs.items():
403
-
404
- if isinstance(value, (DateCriteria, DatetimeCriteria)):
405
- value = getattr(filter, field_name)
406
-
407
- entity_field = getattr(self.entity_type, field_name)
408
-
409
- op_mapping = {
410
- "eq": entity_field == value.value,
411
- "gt": entity_field > value.value,
412
- "lt": entity_field < value.value,
413
- "gte": entity_field >= value.value,
414
- "lte": entity_field <= value.value,
415
- }
416
-
417
- query = query.filter(op_mapping[value.op])
418
- elif isinstance(value, StringCriteria):
419
- value = getattr(filter, field_name)
420
-
421
- entity_field = getattr(self.entity_type, field_name)
422
-
423
- if value.is_exact:
424
- query = query.filter(entity_field == value.value)
425
- else:
426
- query = query.filter(entity_field.contains(value.value))
427
-
428
- return query
429
-
430
-
431
- # 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.23
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=DwBGMHMjD9OcQxeANd4lZh4-u8dMCPL0E273cqbQym4,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=dfZWM-ztN-JEzw8-or7g13XyJ4LwmfbF2d53YFT4dyg,12446
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.23.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
61
- jararaca-0.2.23.dist-info/METADATA,sha256=eSuPU4Xv26Ri6bpmkGeiuK932KFPnjJp10z6bNSW8O4,8552
62
- jararaca-0.2.23.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
63
- jararaca-0.2.23.dist-info/entry_points.txt,sha256=WIh3aIvz8LwUJZIDfs4EeH3VoFyCGEk7cWJurW38q0I,45
64
- jararaca-0.2.23.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.23"
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"