nlbone 0.11.0__py3-none-any.whl → 0.11.3__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,7 +1,8 @@
1
1
  from typing import Any, Callable, List, Optional, Sequence, Type, Union
2
2
 
3
- from sqlalchemy import and_, asc, case, desc, literal, or_
3
+ from sqlalchemy import Select, and_, asc, case, desc, func, literal, or_, select
4
4
  from sqlalchemy.dialects.postgresql import ENUM as PGEnum
5
+ from sqlalchemy.ext.asyncio import AsyncSession
5
6
  from sqlalchemy.orm import Query, Session, aliased
6
7
  from sqlalchemy.orm.attributes import InstrumentedAttribute
7
8
  from sqlalchemy.orm.interfaces import LoaderOption
@@ -384,6 +385,19 @@ def _apply_order(pagination: PaginateRequest, entity, query):
384
385
  return query
385
386
 
386
387
 
388
+ def apply_pagination_async(pagination: PaginateRequest, entity, stmt: Select = None, limit: bool = True) -> Select:
389
+ if stmt is None:
390
+ stmt = select(entity)
391
+
392
+ stmt = _apply_filters(pagination, entity, stmt)
393
+ stmt = _apply_order(pagination, entity, stmt)
394
+
395
+ if limit:
396
+ stmt = stmt.limit(pagination.limit).offset(pagination.offset)
397
+
398
+ return stmt
399
+
400
+
387
401
  def apply_pagination(pagination: PaginateRequest, entity, session: Session, limit=True, query=None) -> Query:
388
402
  if not query:
389
403
  query = session.query(entity)
@@ -437,6 +451,43 @@ def _serialize_item(item: Any, output_cls: OutputType) -> Any:
437
451
  return item
438
452
 
439
453
 
454
+ async def get_paginated_response_async(
455
+ pagination,
456
+ entity,
457
+ session: AsyncSession,
458
+ *,
459
+ with_count: bool = True,
460
+ output_cls: Optional[Type] = None,
461
+ eager_options: Optional[Sequence[LoaderOption]] = None,
462
+ query: Optional[Select] = None,
463
+ ) -> dict:
464
+ stmt = query if query is not None else select(entity)
465
+
466
+ if eager_options:
467
+ stmt = stmt.options(*eager_options)
468
+
469
+ filtered_stmt = apply_pagination_async(pagination, entity, stmt=stmt, limit=False)
470
+
471
+ total_count = None
472
+ if with_count:
473
+ subquery = filtered_stmt.subquery()
474
+ count_stmt = select(func.count()).select_from(subquery)
475
+ total_count = await session.scalar(count_stmt)
476
+
477
+ final_stmt = filtered_stmt.limit(pagination.limit).offset(pagination.offset)
478
+ result = await session.execute(final_stmt)
479
+ rows = result.scalars().all()
480
+
481
+ if output_cls is not None:
482
+ data = [output_cls.model_validate(r, from_attributes=True).model_dump() for r in rows]
483
+ else:
484
+ data = rows
485
+
486
+ return PaginateResponse(
487
+ total_count=total_count, data=data, limit=pagination.limit, offset=pagination.offset
488
+ ).to_dict()
489
+
490
+
440
491
  def get_paginated_response(
441
492
  pagination,
442
493
  entity,
@@ -236,6 +236,7 @@ class SQLAlchemyAsyncRepository(AsyncRepository, ABC):
236
236
  if await self.exists(getattr(entity, "id")):
237
237
  raise ValueError(f"Entity with id={getattr(entity, 'id')!r} already exists")
238
238
  self.session.add(entity)
239
+ await self.session.flush()
239
240
  if self.autocommit:
240
241
  await self.session.commit()
241
242
  return entity
@@ -2,11 +2,61 @@ import inspect
2
2
  from typing import Any, Dict
3
3
 
4
4
  from pydantic import BaseModel
5
+ from sqlalchemy.ext.asyncio import AsyncSession
5
6
 
6
7
  from nlbone.container import Container
7
8
  from nlbone.interfaces.api.additional_filed.field_registry import FieldRule, ResourceRegistry
8
9
 
9
10
 
11
+ async def assemble_response_async(
12
+ obj: Any,
13
+ reg: ResourceRegistry,
14
+ selected_rules: Dict[str, FieldRule],
15
+ session: AsyncSession,
16
+ base_schema: type[BaseModel] | None,
17
+ scope_map: dict[str, set[str]] = None,
18
+ **kwargs,
19
+ ) -> Dict[str, Any]:
20
+ """
21
+ Async version of assemble_response.
22
+ Awaits loaders if they are coroutines.
23
+ """
24
+ base = {f: getattr(obj, f, None) for f in reg.default_fields - set(reg.rules.keys())}
25
+ if base_schema:
26
+ base = base_schema.model_validate(base).model_dump()
27
+
28
+ ctx = {
29
+ "file_service": Container.afiles_service(),
30
+ "entity": obj,
31
+ "db": session,
32
+ "pricing_service": Container.async_pricing_service(),
33
+ **kwargs,
34
+ }
35
+
36
+ roots = {name.split(".", 1)[0] for name in selected_rules.keys()}
37
+
38
+ for root in roots:
39
+ rule = reg.rules.get(root)
40
+ if not rule:
41
+ continue
42
+
43
+ if rule.loader:
44
+ dependencies = ctx | {"scope": scope_map.get(root, {""})} if scope_map else ctx
45
+
46
+ result = inject_dependencies(rule.loader, dependencies=dependencies)
47
+
48
+ if inspect.iscoroutine(result):
49
+ value = await result
50
+ else:
51
+ value = result
52
+ else:
53
+ value = _get_nested_attr(obj, root)
54
+
55
+ _put_nested_key(base, root, value)
56
+
57
+ return base
58
+
59
+
10
60
  def assemble_response(
11
61
  obj: Any,
12
62
  reg: ResourceRegistry,
@@ -1,10 +1,11 @@
1
1
  from dataclasses import dataclass, field
2
2
  from enum import Enum
3
- from typing import Any, Callable, Dict, Iterable, Optional, Set, Tuple, Type
3
+ from typing import Any, Awaitable, Callable, Dict, Iterable, Optional, Set, Tuple, Type
4
4
 
5
5
  from pydantic import BaseModel
6
6
 
7
7
  PermissionChecker = Callable[[Any, str], bool]
8
+ AsyncPermissionChecker = Callable[[str], Awaitable[bool]]
8
9
  Loader = Callable
9
10
 
10
11
 
@@ -2,11 +2,15 @@ from collections import defaultdict
2
2
  from typing import Dict, List, Set, Tuple
3
3
 
4
4
  from nlbone.interfaces.api.additional_filed.field_registry import (
5
+ AsyncPermissionChecker,
5
6
  DefaultsMergeMode,
6
7
  FieldRule,
7
8
  PermissionChecker,
8
9
  ResourceRegistry,
9
10
  )
11
+ from nlbone.interfaces.api.dependencies.async_auth import (
12
+ client_or_user_has_access_func as async_client_or_user_has_access_func,
13
+ )
10
14
  from nlbone.interfaces.api.dependencies.auth import client_or_user_has_access_func
11
15
  from nlbone.interfaces.api.exceptions import BadRequestException, InternalServerException
12
16
 
@@ -36,6 +40,81 @@ class AdditionalFieldsRequest:
36
40
  return [x.strip() for x in s.split(",") if x.strip()]
37
41
 
38
42
 
43
+ async def resolve_requested_fields_async(
44
+ reg: ResourceRegistry,
45
+ additional_fields: AdditionalFieldsRequest = None,
46
+ can: AsyncPermissionChecker = None,
47
+ mode: DefaultsMergeMode = DefaultsMergeMode.UNION,
48
+ ) -> Tuple[Set[str], Dict[str, FieldRule]]:
49
+ if not additional_fields:
50
+ additional_fields = AdditionalFieldsRequest()
51
+ if not can:
52
+ can = async_client_or_user_has_access_func
53
+
54
+ reg.finalize_defaults(mode=mode)
55
+
56
+ expanded_bundles = _expand_bundles(reg, set(additional_fields.bundles))
57
+ requested = set(reg.default_fields) | set(additional_fields.fields) | expanded_bundles
58
+
59
+ # Validation
60
+ unknown = {f for f in requested if (f not in reg.default_fields and f not in reg.rules)}
61
+ if unknown:
62
+ raise BadRequestException(f"unknown_fields: {sorted(unknown)}")
63
+
64
+ selected_rules: Dict[str, FieldRule] = {}
65
+
66
+ for f in requested:
67
+ rule = reg.rules.get(f)
68
+
69
+ if f in reg.default_fields and f not in reg.rules:
70
+ continue
71
+
72
+ if not rule:
73
+ continue
74
+
75
+ # Check Permission
76
+ if rule.permission:
77
+ try:
78
+ if not await can(rule.permission):
79
+ continue
80
+ except Exception:
81
+ continue
82
+
83
+ is_explicit = f in additional_fields.fields
84
+ if not rule.default and not is_explicit:
85
+ continue
86
+
87
+ selected_rules[f] = rule
88
+
89
+ # Dependencies
90
+ final_set = set(requested)
91
+
92
+ def add_deps_recursive(name: str):
93
+ _rule = reg.rules.get(name)
94
+ if not _rule or not _rule.deps:
95
+ return
96
+ for d in _rule.deps:
97
+ if d not in final_set:
98
+ final_set.add(d)
99
+ add_deps_recursive(d)
100
+
101
+ for field_with_rule in list(selected_rules.keys()):
102
+ add_deps_recursive(field_with_rule)
103
+
104
+ parents = {f.split(".", 1)[0] for f in final_set if "." in f}
105
+ for p in parents:
106
+ if p in reg.rules:
107
+ final_set.add(p)
108
+ if p not in selected_rules:
109
+ selected_rules[p] = reg.rules[p]
110
+
111
+ missing_deps = (final_set - set(reg.default_fields)) - set(reg.rules.keys())
112
+ if missing_deps:
113
+ raise InternalServerException(f"registry_missing_rules_for: {sorted(missing_deps)}")
114
+
115
+ return final_set, selected_rules
116
+
117
+
39
118
  def resolve_requested_fields(
40
119
  reg: ResourceRegistry,
41
120
  additional_fields: AdditionalFieldsRequest = None,
@@ -92,7 +92,7 @@ def client_or_user_has_access(*, permissions=None, client_permissions=None):
92
92
  return decorator
93
93
 
94
94
 
95
- async def is_permitted_user(permissions=None):
95
+ def is_permitted_user(permissions=None):
96
96
  async def check_permissions():
97
97
  try:
98
98
  if bypass_authz():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.11.0
3
+ Version: 0.11.3
4
4
  Summary: Backbone package for interfaces and infrastructure in Python projects
5
5
  Author-email: Amir Hosein Kahkbazzadeh <a.khakbazzadeh@gmail.com>
6
6
  License: MIT
@@ -19,8 +19,8 @@ nlbone/adapters/db/postgres/__init__.py,sha256=tvCpHOdZbpQ57o7k-plq7L0e1uZe5_Frb
19
19
  nlbone/adapters/db/postgres/audit.py,sha256=IuWkPitr70UyQ6-GkAedckp8U-Z4cTgzFbdt_bQv1VQ,4800
20
20
  nlbone/adapters/db/postgres/base.py,sha256=I89PsEeR9ADEScG8D5pVSncPrPRBmf-KQQkjajl7Koo,132
21
21
  nlbone/adapters/db/postgres/engine.py,sha256=8bA5qbDN9kcbI2uFitxbD8bseRlI_wQRQQSgfRTH6l8,3812
22
- nlbone/adapters/db/postgres/query_builder.py,sha256=UTjo83NH09g_TIW1Lg_wpTUp62Xvtk4ihP-bqMzfaNI,15839
23
- nlbone/adapters/db/postgres/repository.py,sha256=n01TAzdKd-UbOhirE6KMosuvRdJG2l1cszwVHjTM-Ks,10345
22
+ nlbone/adapters/db/postgres/query_builder.py,sha256=sxl_K_WIDjfi2ISTZxWdY6JLXwj6vjkLLM3Jzp305kw,17440
23
+ nlbone/adapters/db/postgres/repository.py,sha256=SJbrkUcY_MoweBjSQRx4yE4BwqFl0GhTkZVW6VPkHyU,10380
24
24
  nlbone/adapters/db/postgres/schema.py,sha256=NlE7Rr8uXypsw4oWkdZhZwcIBHQEPIpoHLxcUo98i6s,1039
25
25
  nlbone/adapters/db/postgres/types.py,sha256=0SVuIKokog6_ByrYUsYAoIypVM2-uKJhUTeDPtm0qhs,602
26
26
  nlbone/adapters/db/postgres/uow.py,sha256=I4RVeIbGEVhVGcuzhdEUJuX11RhEHTn0egrkCbcsz24,3852
@@ -78,13 +78,13 @@ nlbone/interfaces/api/exceptions.py,sha256=IggZxV9q6l4jqw-G7SWEmuyXnWgbNXJJT-rmn
78
78
  nlbone/interfaces/api/routers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
79
  nlbone/interfaces/api/schemas.py,sha256=34Tz2EeXyf12rFL9iyYWaB2ftuaXUebQQQxSO9ouV94,133
80
80
  nlbone/interfaces/api/additional_filed/__init__.py,sha256=BWemliLSQV9iq1vdUaF733q0FOSipSWBOQk9eYj732Q,318
81
- nlbone/interfaces/api/additional_filed/assembler.py,sha256=E_TGxorIQ-hC5FiCup5WQ0MmbSGfNaypEf5Plc-rjJs,1988
82
- nlbone/interfaces/api/additional_filed/field_registry.py,sha256=IhIvzHWOMtKv8iTdFu7LzQboi4SFTLOyJqKdYPY8xFE,5418
83
- nlbone/interfaces/api/additional_filed/resolver.py,sha256=jv1TIBBHN4LBIMwHGipcy4iq0uP0r6udyaqvhRzb8Bk,4655
81
+ nlbone/interfaces/api/additional_filed/assembler.py,sha256=YsQiui7dr9IAEM5X9KKGDSFeDqA5Zqaj4egoPOWM_ug,3394
82
+ nlbone/interfaces/api/additional_filed/field_registry.py,sha256=bnazgHZ7aO6ZTSaqZKjE8xJOOnbOLi3N19IyR2sV_SA,5487
83
+ nlbone/interfaces/api/additional_filed/resolver.py,sha256=UGZc3hkh6wYjLgzT7lGeGrw8qmaEzouK6Sqk4uSvi34,7125
84
84
  nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py,sha256=LUSAOO3xRUt5ptlraIx7H-7dSkdr1D-WprmnqXRB16g,48
85
85
  nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py,sha256=ecKqPeXZ-YiF14RK9PmK7ln3PCzpCUc18S5zm5IF3fw,339
86
86
  nlbone/interfaces/api/dependencies/__init__.py,sha256=nrmQftdCfKlqSE44R6PkcxtkwCdNpZgI8yLlHyeiACA,274
87
- nlbone/interfaces/api/dependencies/async_auth.py,sha256=c6PohIprT35konFbHQht0y0MJHBouhKqIK_XFQ9Rcbg,3465
87
+ nlbone/interfaces/api/dependencies/async_auth.py,sha256=Suo2rD-zY5MOLnFBaxlqKxp1XI2f3o3W8ULG-4LF8Ok,3459
88
88
  nlbone/interfaces/api/dependencies/auth.py,sha256=4L-hspOyv9HpaCO-rAi7rk52PlZTgMClEG2LA2tMriM,3836
89
89
  nlbone/interfaces/api/dependencies/client_credential.py,sha256=Bo4dYx75Qw0JzTKD9ZfV5EXDEOuwndJk2D-V37K2ePg,1293
90
90
  nlbone/interfaces/api/dependencies/db.py,sha256=-UD39J_86UU7ZJs2ZncpdND0yhAG0NeeeALrgSDuuFw,466
@@ -119,8 +119,8 @@ nlbone/utils/normalize_mobile.py,sha256=sGH4tV9gX-6eVKozviNWJhm1DN1J28Nj-ERldCYk
119
119
  nlbone/utils/read_files.py,sha256=mx8dfvtaaARQFRp_U7OOiERg-GT62h09_lpTzIQsVhs,291
120
120
  nlbone/utils/redactor.py,sha256=-V4HrHmHwPi3Kez587Ek1uJlgK35qGSrwBOvcbw8Jas,1279
121
121
  nlbone/utils/time.py,sha256=DjjyQ9GLsfXoT6NK8RDW2rOlJg3e6sF04Jw6PBUrSvg,1268
122
- nlbone-0.11.0.dist-info/METADATA,sha256=tCSglsRxWG-yPYxEmv_M9TMoZqsq_ixuNCgXXK7jJBM,2295
123
- nlbone-0.11.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
124
- nlbone-0.11.0.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
125
- nlbone-0.11.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
126
- nlbone-0.11.0.dist-info/RECORD,,
122
+ nlbone-0.11.3.dist-info/METADATA,sha256=L0Q66tOht43nYTe90NA2zf_e88iV1ucwgmXac8Us_XM,2295
123
+ nlbone-0.11.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
124
+ nlbone-0.11.3.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
125
+ nlbone-0.11.3.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
126
+ nlbone-0.11.3.dist-info/RECORD,,