rb-commons 0.4.12__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
+ from __future__ import annotations
2
+
1
3
  import uuid
2
- from typing import TypeVar, Type, Generic, Optional, List, Dict, Literal, Union, Sequence
3
- from sqlalchemy import select, delete, update, and_, func, desc, inspect
4
+ from typing import TypeVar, Type, Generic, Optional, List, Dict, Literal, Union, Sequence, Any, Iterable
5
+ from sqlalchemy import select, delete, update, and_, func, desc, inspect, or_
4
6
  from sqlalchemy.exc import IntegrityError, SQLAlchemyError, NoResultFound
5
7
  from sqlalchemy.ext.asyncio import AsyncSession
6
8
  from sqlalchemy.orm import declarative_base, InstrumentedAttribute, selectinload, RelationshipProperty
@@ -10,6 +12,46 @@ from rb_commons.orm.exceptions import DatabaseException, InternalException
10
12
 
11
13
  ModelType = TypeVar('ModelType', bound=declarative_base())
12
14
 
15
+ class Q:
16
+ """Boolean logic container that can be combined with `&`, `|`, and `~`."""
17
+
18
+ def __init__(self, **lookups: Any) -> None:
19
+ self.lookups: Dict[str, Any] = lookups
20
+ self.children: List[Q] = []
21
+ self._operator: str = "AND"
22
+ self.negated: bool = False
23
+
24
+ def _combine(self, other: "Q", operator: str) -> "Q":
25
+ combined = Q()
26
+ combined.children = [self, other]
27
+ combined._operator = operator
28
+ return combined
29
+
30
+ def __or__(self, other: "Q") -> "Q":
31
+ return self._combine(other, "OR")
32
+
33
+ def __and__(self, other: "Q") -> "Q":
34
+ return self._combine(other, "AND")
35
+
36
+ def __invert__(self) -> "Q":
37
+ clone = Q()
38
+ clone.lookups = self.lookups.copy()
39
+ clone.children = list(self.children)
40
+ clone._operator = self._operator
41
+ clone.negated = not self.negated
42
+ return clone
43
+
44
+ def __repr__(self) -> str:
45
+ if self.lookups:
46
+ base = f"Q({self.lookups})"
47
+ else:
48
+ base = "Q()"
49
+ if self.children:
50
+ base += f" {self._operator} {self.children}"
51
+ if self.negated:
52
+ base = f"NOT({base})"
53
+ return base
54
+
13
55
  def with_transaction_error_handling(func):
14
56
  async def wrapper(self, *args, **kwargs):
15
57
  try:
@@ -29,423 +71,293 @@ class BaseManager(Generic[ModelType]):
29
71
  model: Type[ModelType]
30
72
 
31
73
  def __init__(self, session: AsyncSession) -> None:
32
- self.session = session
33
- self.data = None
34
- self.filters = []
35
- self._filtered = False
36
- self._limit = None
74
+ self.session: AsyncSession = session
75
+ self.filters: List[Any] = []
76
+ self._filtered: bool = False
77
+ self._limit: Optional[int] = None
37
78
 
38
79
  async def _smart_commit(self, instance: Optional[ModelType] = None) -> Optional[ModelType]:
39
80
  if not self.session.in_transaction():
40
81
  await self.session.commit()
41
-
42
- if instance:
82
+ if instance is not None:
43
83
  await self.session.refresh(instance)
44
84
  return instance
85
+ return None
86
+
87
+ def _parse_lookup(self, lookup: str, value: Any):
88
+ parts = lookup.split("__")
89
+ operator = "eq"
90
+ if parts[-1] in {"eq", "ne", "gt", "lt", "gte", "lte", "in", "contains", "null"}:
91
+ operator = parts.pop()
92
+
93
+ current: Union[Type[ModelType], InstrumentedAttribute] = self.model
94
+ # walk relationships
95
+ for field in parts:
96
+ attr = getattr(current, field, None)
97
+ if attr is None:
98
+ raise ValueError(f"Invalid filter field: {'.'.join(parts)}")
99
+ if hasattr(attr, "property") and isinstance(attr.property, RelationshipProperty):
100
+ current = attr.property.mapper.class_
101
+ else:
102
+ current = attr
103
+
104
+ if operator == "eq":
105
+ return current == value
106
+ if operator == "ne":
107
+ return current != value
108
+ if operator == "gt":
109
+ return current > value
110
+ if operator == "lt":
111
+ return current < value
112
+ if operator == "gte":
113
+ return current >= value
114
+ if operator == "lte":
115
+ return current <= value
116
+ if operator == "in":
117
+ if not isinstance(value, (list, tuple, set)):
118
+ raise ValueError(f"{lookup}__in requires an iterable")
119
+ return current.in_(value)
120
+ if operator == "contains":
121
+ return current.ilike(f"%{value}%")
122
+ if operator == "null":
123
+ return current.is_(None) if value else current.isnot(None)
124
+ raise ValueError(f"Unsupported operator in lookup: {lookup}")
125
+
126
+ def _q_to_expr(self, q: Q):
127
+ clauses: List[Any] = [self._parse_lookup(k, v) for k, v in q.lookups.items()]
128
+ for child in q.children:
129
+ clauses.append(self._q_to_expr(child))
130
+ combined = (
131
+ True
132
+ if not clauses
133
+ else (or_(*clauses) if q._operator == "OR" else and_(*clauses))
134
+ )
135
+ return ~combined if q.negated else combined
45
136
 
46
- async def get(self, pk: Union[str, int, uuid.UUID], load_relations: Sequence[str] = None) -> Optional[ModelType]:
47
- """
48
- get object based on conditions
49
- """
50
- stmt = select(self.model).filter_by(id=pk)
137
+ def filter(self, *expressions: Any, **lookups: Any) -> "BaseManager[ModelType]":
138
+ """Add **AND** constraints (default behaviour).
51
139
 
52
- if load_relations:
53
- for rel in load_relations:
54
- stmt = stmt.options(selectinload(getattr(self.model, rel)))
55
-
56
- result = await self.session.execute(stmt)
57
- instance = result.scalar_one_or_none()
140
+ * `expressions` can be raw SQLAlchemy clauses **or** `Q` objects.
141
+ * `lookups` are Django‑style keyword filters.
142
+ """
143
+ self._filtered = True
144
+ for expr in expressions:
145
+ self.filters.append(self._q_to_expr(expr) if isinstance(expr, Q) else expr)
146
+ for k, v in lookups.items():
147
+ self.filters.append(self._parse_lookup(k, v))
148
+ return self
58
149
 
59
- if instance is None:
60
- raise NotFoundException(
61
- message="Object does not exist",
62
- status=404,
63
- code="0001",
64
- )
150
+ def or_filter(self, *expressions: Any, **lookups: Any) -> "BaseManager[ModelType]":
151
+ """Add one OR group (shortcut for `filter(Q() | Q())`)."""
152
+ or_clauses: List[Any] = []
153
+ for expr in expressions:
154
+ or_clauses.append(self._q_to_expr(expr) if isinstance(expr, Q) else expr)
155
+ for k, v in lookups.items():
156
+ or_clauses.append(self._parse_lookup(k, v))
157
+ if or_clauses:
158
+ self._filtered = True
159
+ self.filters.append(or_(*or_clauses))
160
+ return self
65
161
 
66
- return instance
162
+ def limit(self, value: int) -> "BaseManager[ModelType]":
163
+ self._limit = value
164
+ return self
67
165
 
68
166
  def _apply_eager_loading(self, stmt, load_all_relations: bool = False):
69
167
  if not load_all_relations:
70
168
  return stmt
71
-
72
- opts = []
73
- visited = set()
169
+ opts: List[Any] = []
170
+ visited: set[Any] = set()
74
171
 
75
172
  def recurse(model, loader=None):
76
173
  mapper = inspect(model)
77
174
  if mapper in visited:
78
175
  return
79
176
  visited.add(mapper)
80
-
81
177
  for rel in mapper.relationships:
82
178
  attr = getattr(model, rel.key)
83
- if loader is None:
84
- this_loader = selectinload(attr)
85
- else:
86
- this_loader = loader.selectinload(attr)
179
+ this_loader = selectinload(attr) if loader is None else loader.selectinload(attr)
87
180
  opts.append(this_loader)
88
181
  recurse(rel.mapper.class_, this_loader)
89
182
 
90
183
  recurse(self.model)
91
184
  return stmt.options(*opts)
92
185
 
93
- def filter(self, **kwargs) -> 'BaseManager[ModelType]':
94
- """
95
- Dynamically apply filters to the query.
96
-
97
- Supported operators:
98
- - __eq (e.g., field__eq=value) or just field=value
99
- - __ne (field__ne=value)
100
- - __gt (field__gt=value)
101
- - __lt (field__lt=value)
102
- - __gte (field__gte=value)
103
- - __lte (field__lte=value)
104
- - __in (field__in=[val1, val2, ...])
105
- - __contains (field__contains='text')
106
- - __null (field__null=True/False) - True for IS NULL, False for IS NOT NULL
107
-
108
- Additionally supports nested paths, e.g.,
109
- product__shop_id=None
110
- product__shop__country='US'
111
- """
112
- self._filtered = True
113
- self.filters = []
114
-
115
- for key, value in kwargs.items():
116
- parts = key.split("__")
117
-
118
- operator = "eq"
119
-
120
- if parts[-1] in {"eq", "ne", "gt", "lt", "gte", "lte", "in", "contains", "null"}:
121
- operator = parts.pop()
122
-
123
- current_attr = self.model
124
-
125
- for field_name in parts:
126
- attr_candidate = getattr(current_attr, field_name, None)
127
- if attr_candidate is None:
128
- raise ValueError(f"Invalid filter field: {'.'.join(parts)}")
129
-
130
- if hasattr(attr_candidate, "property") and isinstance(attr_candidate.property, RelationshipProperty):
131
- current_attr = attr_candidate.property.mapper.class_
132
- else:
133
- current_attr = attr_candidate
134
-
135
- if operator == "eq":
136
- # e.g., column == value
137
- self.filters.append(current_attr == value)
138
-
139
- elif operator == "ne":
140
- # e.g., column != value
141
- self.filters.append(current_attr != value)
142
-
143
- elif operator == "gt":
144
- self.filters.append(current_attr > value)
145
-
146
- elif operator == "lt":
147
- self.filters.append(current_attr < value)
148
-
149
- elif operator == "gte":
150
- self.filters.append(current_attr >= value)
151
-
152
- elif operator == "lte":
153
- self.filters.append(current_attr <= value)
154
-
155
- elif operator == "in":
156
- if not isinstance(value, list):
157
- raise ValueError(f"{'.'.join(parts)}__in requires a list, got {type(value)}")
158
- self.filters.append(current_attr.in_(value))
159
-
160
- elif operator == "contains":
161
- # e.g., column ILIKE %value%
162
- self.filters.append(current_attr.ilike(f"%{value}%"))
163
-
164
- elif operator == "null":
165
- if value is True:
166
- self.filters.append(current_attr.is_(None))
167
- else:
168
- self.filters.append(current_attr.isnot(None))
169
-
170
- return self
171
-
172
- def _ensure_filtered(self):
173
- """Ensure that `filter()` has been called before using certain methods."""
174
- if not self._filtered:
175
- raise RuntimeError("You must call `filter()` before using this method.")
176
-
177
- async def _execute_query_and_unique_data(self, stmt, load_all_relations: bool) -> List[ModelType]:
186
+ async def _execute_query(self, stmt, load_all_relations: bool):
178
187
  stmt = self._apply_eager_loading(stmt, load_all_relations)
179
188
  result = await self.session.execute(stmt)
180
189
  rows = result.scalars().all()
181
- unique_by_pk = {obj.id: obj for obj in rows}
182
- return list(unique_by_pk.values())
190
+ return list({obj.id: obj for obj in rows}.values())
183
191
 
184
- async def all(self, load_all_relations: bool = False) -> List[ModelType]:
185
- self._ensure_filtered()
186
- stmt = select(self.model).filter(and_(*self.filters))
192
+ def _reset_state(self):
193
+ self.filters.clear()
194
+ self._filtered = False
195
+ self._limit = None
187
196
 
197
+ async def all(self, load_all_relations: bool = False):
198
+ stmt = select(self.model)
199
+ if self.filters:
200
+ stmt = stmt.filter(and_(*self.filters))
188
201
  if self._limit:
189
202
  stmt = stmt.limit(self._limit)
190
- self._limit = None
191
-
192
- return await self._execute_query_and_unique_data(stmt, load_all_relations)
193
-
194
- async def paginate(self, limit: int = 10, offset: int = 0, load_all_relations: bool = False) -> List[ModelType]:
195
- self._ensure_filtered()
196
- stmt = select(self.model).filter(and_(*self.filters)).limit(limit).offset(offset)
197
- return await self._execute_query_and_unique_data(stmt, load_all_relations)
198
-
199
- def limit(self, value: int) -> 'BaseManager[ModelType]':
200
- """
201
- Set a limit on the number of results returned by queries like `all()` or `first()`.
202
- """
203
- self._limit = value
204
- return self
203
+ try:
204
+ return await self._execute_query(stmt, load_all_relations)
205
+ finally:
206
+ self._reset_state()
205
207
 
206
- async def first(self, load_relations: Sequence[str] = None) -> Optional[ModelType]:
207
- """Return the first matching object, or None."""
208
+ async def first(self, load_relations: Optional[Sequence[str]] = None):
208
209
  self._ensure_filtered()
209
-
210
210
  stmt = select(self.model).filter(and_(*self.filters))
211
-
212
211
  if load_relations:
213
212
  for rel in load_relations:
214
213
  stmt = stmt.options(selectinload(getattr(self.model, rel)))
215
-
216
214
  result = await self.session.execute(stmt)
215
+ self._reset_state()
217
216
  return result.scalars().first()
218
217
 
219
- async def last(self, load_relations: Sequence[str] = None) -> Optional[ModelType]:
220
- """Return the last matching object, or None."""
221
-
218
+ async def last(self, load_relations: Optional[Sequence[str]] = None):
222
219
  self._ensure_filtered()
223
-
224
- stmt = select(self.model).filter(and_(*self.filters)).order_by(desc(self.model.id))
225
-
220
+ stmt = (
221
+ select(self.model)
222
+ .filter(and_(*self.filters))
223
+ .order_by(desc(self.model.id))
224
+ )
226
225
  if load_relations:
227
226
  for rel in load_relations:
228
227
  stmt = stmt.options(selectinload(getattr(self.model, rel)))
229
-
230
228
  result = await self.session.execute(stmt)
229
+ self._reset_state()
231
230
  return result.scalars().first()
232
231
 
233
232
  async def count(self) -> int:
234
- """Return the count of matching records."""
235
-
236
233
  self._ensure_filtered()
237
234
  query = select(func.count()).select_from(self.model).filter(and_(*self.filters))
238
235
  result = await self.session.execute(query)
236
+ self._reset_state()
239
237
  return result.scalar_one()
240
238
 
239
+ async def paginate(
240
+ self,
241
+ limit: int = 10,
242
+ offset: int = 0,
243
+ load_all_relations: bool = False,
244
+ ):
245
+ self._ensure_filtered()
246
+ stmt = (
247
+ select(self.model)
248
+ .filter(and_(*self.filters))
249
+ .limit(limit)
250
+ .offset(offset)
251
+ )
252
+ try:
253
+ return await self._execute_query(stmt, load_all_relations)
254
+ finally:
255
+ self._reset_state()
256
+
241
257
  @with_transaction_error_handling
242
- async def create(self, **kwargs) -> ModelType:
243
- """
244
- Create a new object
245
- """
258
+ async def create(self, **kwargs):
246
259
  obj = self.model(**kwargs)
247
-
248
260
  self.session.add(obj)
249
261
  await self.session.flush()
250
262
  return await self._smart_commit(obj)
251
263
 
252
264
  @with_transaction_error_handling
253
- async def delete(self, instance: ModelType = None) -> bool:
254
- """
255
- Delete object(s) with flexible filtering options
256
- - If `instance` is provided, delete that single instance.
257
- - If `instance` is not provided, delete according to self.filters.
258
-
259
- :arg instance: Model instance to delete.
260
- :return: Number of deleted records or None
261
- """
262
-
263
- if instance is not None:
264
- await self.session.delete(instance)
265
- await self.session.commit()
266
- return True
267
-
268
- self._ensure_filtered()
269
-
270
- try:
271
- delete_stmt = delete(self.model).where(and_(*self.filters))
272
- await self.session.execute(delete_stmt)
273
- await self.session.commit()
274
- return True
275
- except NoResultFound:
276
- return False
277
-
278
- @with_transaction_error_handling
279
- async def bulk_delete(self) -> int:
280
- """
281
- Bulk delete with flexible filtering.
282
-
283
- Automatically commits if not inside a transaction.
284
-
285
- :return: Number of deleted records
286
- """
287
- self._ensure_filtered()
288
-
289
- delete_stmt = delete(self.model).where(and_(*self.filters))
290
- result = await self.session.execute(delete_stmt)
291
- await self._smart_commit()
292
- return result.rowcount # ignore
265
+ async def save(self, instance: ModelType):
266
+ self.session.add(instance)
267
+ await self.session.flush()
268
+ return await self._smart_commit(instance)
293
269
 
294
270
  @with_transaction_error_handling
295
- async def update_by_filters(self, filters: Dict, **update_fields) -> Optional[ModelType]:
296
- """
297
- Update object(s) with flexible filtering options
298
-
299
- :param filters: Conditions for selecting records to update
300
- :param update_fields: Fields and values to update
301
- :return: Number of updated records
302
- """
303
- if not update_fields:
271
+ async def update(self, instance: ModelType, **fields):
272
+ if not fields:
304
273
  raise InternalException("No fields provided for update")
305
-
306
- update_stmt = update(self.model).filter_by(**filters).values(**update_fields)
307
- await self.session.execute(update_stmt)
308
- await self.session.commit()
309
- updated_instance = await self.get(**filters)
310
- return updated_instance
311
-
312
- @with_transaction_error_handling
313
- async def update(self, instance: ModelType, **update_fields) -> Optional[ModelType]:
314
- """
315
- Update an existing database instance with new fields
316
-
317
- :param instance: The database model instance to update
318
- :param update_fields: Keyword arguments of fields to update
319
- :return: The updated instance
320
-
321
- :raises InternalException: If integrity violation
322
- :raises DatabaseException: For database-related errors
323
- """
324
- # Validate update fields
325
- if not update_fields:
326
- raise InternalException("No fields provided for update")
327
-
328
- # Apply updates directly to the instance
329
- for key, value in update_fields.items():
330
- setattr(instance, key, value)
331
-
274
+ for k, v in fields.items():
275
+ setattr(instance, k, v)
332
276
  self.session.add(instance)
333
277
  await self._smart_commit()
334
-
335
278
  return instance
336
279
 
337
280
  @with_transaction_error_handling
338
- async def save(self, instance: ModelType) -> Optional[ModelType]:
339
- """
340
- Save instance
341
-
342
- :param instance: The database model instance to save
343
- :return: The saved instance
344
-
345
- Automatically commits if not inside a transaction.
346
-
347
- :raises InternalException: If integrity violation
348
- :raises DatabaseException: For database-related errors
349
- """
350
- self.session.add(instance)
351
- await self.session.flush()
352
- return await self._smart_commit(instance)
281
+ async def update_by_filters(self, filters: Dict[str, Any], **fields):
282
+ if not fields:
283
+ raise InternalException("No fields provided for update")
284
+ stmt = update(self.model).filter_by(**filters).values(**fields)
285
+ await self.session.execute(stmt)
286
+ await self.session.commit()
287
+ return await self.get(**filters)
353
288
 
354
289
  @with_transaction_error_handling
355
- async def lazy_save(self, instance: ModelType, load_relations: Sequence[str] = None) -> Optional[ModelType]:
356
- self.session.add(instance)
357
- await self.session.flush()
358
- await self._smart_commit(instance)
359
-
360
- if load_relations is None:
361
- mapper = inspect(self.model)
362
- load_relations = [rel.key for rel in mapper.relationships]
363
-
364
- if not load_relations:
365
- return instance
366
-
367
- stmt = select(self.model).filter_by(id=instance.id)
368
-
369
- for rel in load_relations:
370
- stmt = stmt.options(selectinload(getattr(self.model, rel)))
371
-
372
- result = await self.session.execute(stmt)
373
- loaded_instance = result.scalar_one_or_none()
374
-
375
- if loaded_instance is None:
376
- raise NotFoundException(
377
- message="Object saved but could not be retrieved with relationships",
378
- status=404,
379
- code="0001",
380
- )
381
-
382
- return loaded_instance
383
-
384
- async def is_exists(self, **kwargs) -> bool:
385
- stmt = select(self.model).filter_by(**kwargs)
386
- result = await self.session.execute(stmt)
387
- return result.scalar_one_or_none() is not None
290
+ async def delete(self, instance: Optional[ModelType] = None):
291
+ if instance is not None:
292
+ await self.session.delete(instance)
293
+ await self.session.commit()
294
+ return True
295
+ self._ensure_filtered()
296
+ stmt = delete(self.model).where(and_(*self.filters))
297
+ await self.session.execute(stmt)
298
+ await self.session.commit()
299
+ self._reset_state()
300
+ return True
388
301
 
389
302
  @with_transaction_error_handling
390
- async def bulk_save(self, instances: List[ModelType]) -> None:
391
- """
392
- Bulk save a list of instances into the database.
393
-
394
- If inside a transaction, flushes only.
395
- If not in a transaction, commits after flush.
396
-
397
- :param instances: List of instances
398
- :raises DatabaseException: If any database error occurs
399
- :raises InternalException: If any unexpected error occurs
400
- """
303
+ async def bulk_save(self, instances: Iterable[ModelType]):
401
304
  if not instances:
402
305
  return
403
-
404
- self.session.add_all(instances)
306
+ self.session.add_all(list(instances))
405
307
  await self.session.flush()
406
-
407
308
  if not self.session.in_transaction():
408
309
  await self.session.commit()
409
310
 
410
- def has_relation(self, relation_name: str) -> 'BaseManager[ModelType]':
411
- """
412
- Check if a relationship exists between models using an EXISTS subquery.
413
-
414
- :param relation_name Name of the relationship to check. Must be a valid relationship
415
- defined in the model.
416
-
417
- :return BaseManager[ModelType]: Self instance for method chaining.
311
+ @with_transaction_error_handling
312
+ async def bulk_delete(self):
313
+ self._ensure_filtered()
314
+ stmt = delete(self.model).where(and_(*self.filters))
315
+ result = await self.session.execute(stmt)
316
+ await self._smart_commit()
317
+ self._reset_state()
318
+ return result.rowcount
319
+
320
+ async def get(
321
+ self,
322
+ pk: Union[str, int, uuid.UUID],
323
+ load_relations: Optional[Sequence[str]] = None,
324
+ ):
325
+ stmt = select(self.model).filter_by(id=pk)
326
+ if load_relations:
327
+ for rel in load_relations:
328
+ stmt = stmt.options(selectinload(getattr(self.model, rel)))
329
+ result = await self.session.execute(stmt)
330
+ instance = result.scalar_one_or_none()
331
+ if instance is None:
332
+ raise NotFoundException("Object does not exist", 404, "0001")
333
+ return instance
418
334
 
419
- :raise DatabaseException: If there's an error constructing the subquery.
420
- :raise InternalException: If there's an unexpected error in relationship handling.
335
+ async def is_exists(self, **lookups):
336
+ stmt = select(self.model).filter_by(**lookups)
337
+ result = await self.session.execute(stmt)
338
+ return result.scalar_one_or_none() is not None
421
339
 
422
- Notes:
423
- - The relationship must be properly defined in the SQLAlchemy model
424
- - Uses a non-correlated EXISTS subquery for better performance
425
- - Silently continues if the relationship doesn't exist
426
- """
427
- # Get the relationship property
340
+ def has_relation(self, relation_name: str):
428
341
  relationship = getattr(self.model, relation_name)
429
-
430
- # Create subquery using select
431
342
  subquery = (
432
343
  select(1)
433
344
  .select_from(relationship.property.mapper.class_)
434
345
  .where(relationship.property.primaryjoin)
435
346
  .exists()
436
347
  )
437
-
438
- # Add the exists condition to filters
439
348
  self.filters.append(subquery)
440
-
349
+ self._filtered = True
441
350
  return self
442
351
 
443
- def model_to_dict(self, instance: ModelType, exclude: set = None):
352
+ def model_to_dict(self, instance: ModelType, exclude: set[str] | None = None):
444
353
  exclude = exclude or set()
445
-
446
354
  return {
447
- c.key: getattr(instance, c.key)
448
- for c in inspect(instance).mapper.column_attrs
449
- if c.key not in exclude
355
+ col.key: getattr(instance, col.key)
356
+ for col in inspect(instance).mapper.column_attrs
357
+ if col.key not in exclude
450
358
  }
451
359
 
360
+ def _ensure_filtered(self):
361
+ if not self._filtered:
362
+ raise RuntimeError("You must call `filter()` before this operation.")
363
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rb-commons
3
- Version: 0.4.12
3
+ Version: 0.5.0
4
4
  Summary: Commons of project and simplified orm based on sqlalchemy.
5
5
  Home-page: https://github.com/RoboSell-organization/rb-commons
6
6
  Author: Abdulvoris
@@ -11,7 +11,7 @@ rb_commons/http/consul.py,sha256=Ioq72VD1jGwoC96set7n2SgxN40olzI-myA2lwKkYi4,186
11
11
  rb_commons/http/exceptions.py,sha256=EGRMr1cRgiJ9Q2tkfANbf0c6-zzXf1CD6J3cmCaT_FA,1885
12
12
  rb_commons/orm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  rb_commons/orm/exceptions.py,sha256=1aMctiEwrPjyehoXVX1l6ML5ZOhmDkmBISzlTD5ey1Y,509
14
- rb_commons/orm/managers.py,sha256=vQ1S7g_wM0sg467HjJh6en-s5qmoO6i4-grjBSZCtgU,16634
14
+ rb_commons/orm/managers.py,sha256=Tk33tD4l94IkliHFBR_BRy-jrc-JgVh9S_mXn8v0rpM,13620
15
15
  rb_commons/orm/services.py,sha256=71eRcJ4TxZvzNz-hLXo12X4U7PGK54ZfbLAb27AjZi8,1589
16
16
  rb_commons/permissions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  rb_commons/permissions/role_permissions.py,sha256=HL50s5RHW1nk2U3-YTESg0EDGkJDu1vN4QchcfK-L5g,988
@@ -20,7 +20,7 @@ rb_commons/schemes/jwt.py,sha256=F66JJDhholuOPPzlKeoC6f1TL4gXg4oRUrV5yheNpyo,167
20
20
  rb_commons/schemes/pagination.py,sha256=8VZW1wZGJIPR9jEBUgppZUoB4uqP8ORudHkMwvEJSxg,1866
21
21
  rb_commons/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  rb_commons/utils/media.py,sha256=KNY_9SdRa3Rp7d3B1tZaXkhmzVa65RcS62BYwZP1bVM,332
23
- rb_commons-0.4.12.dist-info/METADATA,sha256=nOLVXYvtQKAzUVihH3SaKNNF7AUQsUOjxCezGQ_vECw,6571
24
- rb_commons-0.4.12.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
25
- rb_commons-0.4.12.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
26
- rb_commons-0.4.12.dist-info/RECORD,,
23
+ rb_commons-0.5.0.dist-info/METADATA,sha256=bT0B3meXrn9ME0juZhAq0XFg58MaAJxltKGBzpSHEjg,6570
24
+ rb_commons-0.5.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
25
+ rb_commons-0.5.0.dist-info/top_level.txt,sha256=HPx_WAYo3_fbg1WCeGHsz3wPGio1ucbnrlm2lmqlJog,11
26
+ rb_commons-0.5.0.dist-info/RECORD,,