nlbone 0.6.14__tar.gz → 0.6.15__tar.gz
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.
- {nlbone-0.6.14 → nlbone-0.6.15}/PKG-INFO +1 -1
- {nlbone-0.6.14 → nlbone-0.6.15}/pyproject.toml +1 -1
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/postgres/query_builder.py +40 -8
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/http_clients/pricing/pricing_service.py +4 -4
- nlbone-0.6.15/src/nlbone/interfaces/api/additional_filed/__init__.py +7 -0
- nlbone-0.6.15/src/nlbone/interfaces/api/additional_filed/assembler.py +54 -0
- nlbone-0.6.15/src/nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py +1 -0
- nlbone-0.6.15/src/nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py +13 -0
- nlbone-0.6.15/src/nlbone/interfaces/api/additional_filed/field_registry.py +105 -0
- nlbone-0.6.15/src/nlbone/interfaces/api/additional_filed/resolver.py +132 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/dependencies/auth.py +33 -32
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/exceptions.py +8 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/pagination/__init__.py +2 -2
- nlbone-0.6.15/src/nlbone/interfaces/api/schema/adaptive_schema.py +114 -0
- nlbone-0.6.15/src/nlbone/interfaces/api/schema/base_response_model.py +22 -0
- nlbone-0.6.15/src/nlbone/interfaces/api/schemas.py +5 -0
- nlbone-0.6.14/src/nlbone/interfaces/api/schema/adaptive_schema.py +0 -78
- nlbone-0.6.14/src/nlbone/interfaces/api/schema/base_response_model.py +0 -5
- nlbone-0.6.14/src/nlbone/interfaces/api/schemas.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/.gitignore +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/LICENSE +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/README.md +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/auth/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/auth/keycloak.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/auth/token_provider.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/cache/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/cache/async_redis.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/cache/memory.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/cache/pubsub_listener.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/cache/redis.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/postgres/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/postgres/audit.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/postgres/base.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/postgres/engine.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/postgres/repository.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/postgres/schema.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/postgres/uow.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/redis/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/db/redis/client.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/http_clients/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/http_clients/pricing/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/http_clients/uploadchi/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/http_clients/uploadchi/uploadchi.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/messaging/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/messaging/event_bus.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/messaging/redis.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/percolation/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/percolation/connection.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/config/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/config/logging.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/config/settings.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/container.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/application/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/application/base_worker.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/application/events.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/application/services/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/application/services.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/application/use_case.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/domain/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/domain/base.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/domain/events.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/domain/models.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/ports/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/ports/auth.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/ports/cache.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/ports/event_bus.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/ports/files.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/ports/messaging.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/ports/repo.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/core/ports/uow.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/dependencies/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/dependencies/async_auth.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/dependencies/uow.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/exception_handlers.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/middleware/authentication.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/routers.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/api/schema/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/cli/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/cli/init_db.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/cli/main.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/jobs/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/types.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/utils/__init__.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/utils/cache.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/utils/cache_keys.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/utils/cache_registry.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/utils/context.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/utils/http.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/utils/redactor.py +0 -0
- {nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/utils/time.py +0 -0
|
@@ -137,13 +137,15 @@ def _to_sql_like_pattern(s: str) -> str:
|
|
|
137
137
|
|
|
138
138
|
def _parse_field_and_op(field: str):
|
|
139
139
|
"""
|
|
140
|
-
Supports
|
|
141
|
-
|
|
140
|
+
Supports suffix operators like:
|
|
141
|
+
__ilike, __gte, __lte, __lt, __gt, __ne
|
|
142
|
+
Returns (base_field, op) where op in {'eq','ilike','gte','lte','lt','gt','ne'}
|
|
142
143
|
"""
|
|
143
144
|
if "__" in field:
|
|
144
145
|
base, op = field.rsplit("__", 1)
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
op = op.lower()
|
|
147
|
+
if base and op in {"ilike", "gte", "lte", "lt", "gt", "ne"}:
|
|
148
|
+
return base, op
|
|
147
149
|
return field, "eq"
|
|
148
150
|
|
|
149
151
|
|
|
@@ -214,10 +216,24 @@ def _apply_filters(pagination, entity, query):
|
|
|
214
216
|
patterns = [_to_sql_like_pattern(str(v)) for v in vals]
|
|
215
217
|
predicates.append(or_(*[col.ilike(p) for p in patterns]))
|
|
216
218
|
else:
|
|
217
|
-
coerced = [coerce(v) for v in vals]
|
|
219
|
+
coerced = [coerce(v) for v in vals if v is not None]
|
|
218
220
|
if not coerced:
|
|
219
221
|
continue
|
|
220
|
-
|
|
222
|
+
|
|
223
|
+
if op_hint == "eq":
|
|
224
|
+
predicates.append(col.in_(coerced))
|
|
225
|
+
elif op_hint == "ne":
|
|
226
|
+
predicates.append(or_(*[col != v for v in coerced]))
|
|
227
|
+
elif op_hint == "gt":
|
|
228
|
+
predicates.append(or_(*[col > v for v in coerced]))
|
|
229
|
+
elif op_hint == "gte":
|
|
230
|
+
predicates.append(or_(*[col >= v for v in coerced]))
|
|
231
|
+
elif op_hint == "lt":
|
|
232
|
+
predicates.append(or_(*[col < v for v in coerced]))
|
|
233
|
+
elif op_hint == "lte":
|
|
234
|
+
predicates.append(or_(*[col <= v for v in coerced]))
|
|
235
|
+
else:
|
|
236
|
+
predicates.append(col.in_(coerced))
|
|
221
237
|
else:
|
|
222
238
|
if _use_ilike(value) and _is_text_type(coltype):
|
|
223
239
|
pattern = _to_sql_like_pattern(str(value))
|
|
@@ -225,9 +241,25 @@ def _apply_filters(pagination, entity, query):
|
|
|
225
241
|
else:
|
|
226
242
|
v = coerce(value)
|
|
227
243
|
if v is None:
|
|
228
|
-
|
|
244
|
+
if op_hint in {"eq", "ilike"}:
|
|
245
|
+
predicates.append(col.is_(None))
|
|
246
|
+
else:
|
|
247
|
+
continue
|
|
229
248
|
else:
|
|
230
|
-
|
|
249
|
+
if op_hint == "eq":
|
|
250
|
+
predicates.append(col == v)
|
|
251
|
+
elif op_hint == "ne":
|
|
252
|
+
predicates.append(col != v)
|
|
253
|
+
elif op_hint == "gt":
|
|
254
|
+
predicates.append(col > v)
|
|
255
|
+
elif op_hint == "gte":
|
|
256
|
+
predicates.append(col >= v)
|
|
257
|
+
elif op_hint == "lt":
|
|
258
|
+
predicates.append(col < v)
|
|
259
|
+
elif op_hint == "lte":
|
|
260
|
+
predicates.append(col <= v)
|
|
261
|
+
else:
|
|
262
|
+
predicates.append(col == v)
|
|
231
263
|
|
|
232
264
|
except _InvalidEnum as e:
|
|
233
265
|
raise UnprocessableEntityException(str(e), loc=["query", "filters", raw_field]) from e
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Dict, List, Literal, Optional, Union
|
|
3
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
import requests
|
|
@@ -16,7 +16,7 @@ class PricingError(Exception):
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class CalculatePriceIn(BaseModel):
|
|
19
|
-
params: dict[str,
|
|
19
|
+
params: dict[str, Any]
|
|
20
20
|
product_id: NonNegativeInt | None = None
|
|
21
21
|
product_title: str | None = None
|
|
22
22
|
|
|
@@ -51,8 +51,8 @@ class Formula(BaseModel):
|
|
|
51
51
|
id: int
|
|
52
52
|
title: str
|
|
53
53
|
key: str
|
|
54
|
-
status: str
|
|
55
|
-
description: str
|
|
54
|
+
status: str | None
|
|
55
|
+
description: str | None
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
class PricingRule(BaseModel):
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
from nlbone.interfaces.api.additional_filed.assembler import assemble_response
|
|
2
|
+
from nlbone.interfaces.api.additional_filed.field_registry import FieldRule, ResourceRegistry
|
|
3
|
+
from nlbone.interfaces.api.additional_filed.resolver import (
|
|
4
|
+
AdditionalFieldsRequest,
|
|
5
|
+
build_query_plan,
|
|
6
|
+
resolve_requested_fields,
|
|
7
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Any, Dict
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from nlbone.container import Container
|
|
7
|
+
from nlbone.interfaces.api.additional_filed.field_registry import FieldRule, ResourceRegistry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def assemble_response(
|
|
11
|
+
obj: Any,
|
|
12
|
+
reg: ResourceRegistry,
|
|
13
|
+
selected_rules: Dict[str, FieldRule],
|
|
14
|
+
base_schema: type[BaseModel] | None,
|
|
15
|
+
session,
|
|
16
|
+
) -> Dict[str, Any]:
|
|
17
|
+
base = {f: getattr(obj, f, None) for f in reg.default_fields - set(reg.rules.keys())}
|
|
18
|
+
if base_schema:
|
|
19
|
+
base = base_schema.model_validate(base).model_dump()
|
|
20
|
+
|
|
21
|
+
ctx = {"file_service": Container.file_service(), "entity": obj, "db": session}
|
|
22
|
+
for name, rule in selected_rules.items():
|
|
23
|
+
if rule.loader:
|
|
24
|
+
value = inject_dependencies(rule.loader, dependencies=ctx)
|
|
25
|
+
else:
|
|
26
|
+
value = _get_nested_attr(obj, name)
|
|
27
|
+
_put_nested_key(base, name, value)
|
|
28
|
+
|
|
29
|
+
return base
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def inject_dependencies(handler, dependencies):
|
|
33
|
+
params = inspect.signature(handler).parameters
|
|
34
|
+
deps = {name: dependency for name, dependency in dependencies.items() if name in params}
|
|
35
|
+
return handler(**deps)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _get_nested_attr(obj: Any, dotted: str):
|
|
39
|
+
cur = obj
|
|
40
|
+
for part in dotted.split("."):
|
|
41
|
+
if cur is None:
|
|
42
|
+
return None
|
|
43
|
+
cur = getattr(cur, part, None)
|
|
44
|
+
return cur
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _put_nested_key(base: Dict[str, Any], dotted: str, value: Any):
|
|
48
|
+
parts = dotted.split(".")
|
|
49
|
+
cur = base
|
|
50
|
+
for p in parts[:-1]:
|
|
51
|
+
if p not in cur or not isinstance(cur[p], dict):
|
|
52
|
+
cur[p] = {}
|
|
53
|
+
cur = cur[p]
|
|
54
|
+
cur[parts[-1]] = value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .image_field_rules import IMAGE_FILED_RULE
|
nlbone-0.6.15/src/nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from nlbone.interfaces.api.additional_filed import FieldRule
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_image(entity, file_service):
|
|
5
|
+
image_id = entity.image_id
|
|
6
|
+
if image_id:
|
|
7
|
+
try:
|
|
8
|
+
return file_service.get_file(image_id)
|
|
9
|
+
except Exception:
|
|
10
|
+
return None
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
IMAGE_FILED_RULE = FieldRule(default=True, name="image", loader=get_image)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Callable, Dict, Iterable, Optional, Set, Tuple, Type
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
PermissionChecker = Callable[[Any, str], bool]
|
|
8
|
+
Loader = Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _schema_fields(schema: Type[BaseModel], by_alias: bool = True) -> Set[str]:
|
|
12
|
+
names = set()
|
|
13
|
+
for name, f in schema.model_fields.items():
|
|
14
|
+
names.add(f.alias or name if by_alias else name)
|
|
15
|
+
return names
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class FieldRule:
|
|
20
|
+
"""
|
|
21
|
+
name: field name (nested with dot)
|
|
22
|
+
permission: None for public
|
|
23
|
+
deps: dependencies
|
|
24
|
+
columns: relationship (SQLAlchemy Column/InstrumentedAttribute)
|
|
25
|
+
join_paths: required paths for eager-load (ex. Product.supplier, Supplier.address)
|
|
26
|
+
loader:
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
name: str
|
|
30
|
+
default: bool = False
|
|
31
|
+
permission: Optional[str] = None
|
|
32
|
+
deps: Tuple[str, ...] = ()
|
|
33
|
+
columns: Tuple[Any, ...] = ()
|
|
34
|
+
join_paths: Tuple[Any, ...] = ()
|
|
35
|
+
loader: Optional[Loader] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DefaultsMergeMode(str, Enum):
|
|
39
|
+
STRICT = "strict" # schema == rules; else raise
|
|
40
|
+
SCHEMA_PRIORITY = "schema" # defaults = schema_fields
|
|
41
|
+
RULES_PRIORITY = "rules" # defaults = rules_default_fields
|
|
42
|
+
UNION = "union" # defaults = schema ∪ rules
|
|
43
|
+
INTERSECTION = "intersection" # defaults = schema ∩ rules
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ResourceRegistry:
|
|
48
|
+
resource: str
|
|
49
|
+
default_fields: Set[str] = field(default_factory=set)
|
|
50
|
+
base_schema: Optional[Type[BaseModel]] = None
|
|
51
|
+
rules: Dict[str, FieldRule] = field(default_factory=dict)
|
|
52
|
+
bundles: Dict[str, Set[str]] = field(default_factory=dict)
|
|
53
|
+
_schema_fields_cache: Set[str] = field(default_factory=set, repr=False)
|
|
54
|
+
|
|
55
|
+
def add_rule(self, rule: FieldRule) -> "ResourceRegistry":
|
|
56
|
+
self.rules[rule.name] = rule
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
def add_bundle(self, bundle: str, items: Iterable[str]) -> "ResourceRegistry":
|
|
60
|
+
self.bundles[bundle] = set(items)
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def set_defaults(self, fields: Iterable[str]) -> "ResourceRegistry":
|
|
64
|
+
self.default_fields = set(fields)
|
|
65
|
+
return self
|
|
66
|
+
|
|
67
|
+
def recompute_defaults_from_rules(self) -> "ResourceRegistry":
|
|
68
|
+
self.default_fields = {r.name for r in self.rules.values() if r.default}
|
|
69
|
+
return self
|
|
70
|
+
|
|
71
|
+
def from_paydantc(self, schema: Type[BaseModel], *, by_alias: bool = True) -> "ResourceRegistry":
|
|
72
|
+
self.base_schema = schema
|
|
73
|
+
self._schema_fields_cache = _schema_fields(schema, by_alias=by_alias)
|
|
74
|
+
return self
|
|
75
|
+
|
|
76
|
+
def finalize_defaults(self, mode: DefaultsMergeMode = DefaultsMergeMode.UNION) -> "ResourceRegistry":
|
|
77
|
+
schema_set: Set[str] = self._schema_fields_cache if self.base_schema else set()
|
|
78
|
+
rules_set: Set[str] = {r.name for r in self.rules.values() if r.default}
|
|
79
|
+
|
|
80
|
+
manual_set: Set[str] = set(self.default_fields)
|
|
81
|
+
|
|
82
|
+
if mode == DefaultsMergeMode.STRICT:
|
|
83
|
+
if self.base_schema is None or not rules_set:
|
|
84
|
+
base = schema_set or rules_set
|
|
85
|
+
else:
|
|
86
|
+
if schema_set != rules_set:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
f"[{self.resource}] Default fields diverged: "
|
|
89
|
+
f"schema={sorted(schema_set)} vs rules={sorted(rules_set)}"
|
|
90
|
+
)
|
|
91
|
+
base = schema_set
|
|
92
|
+
elif mode == DefaultsMergeMode.SCHEMA_PRIORITY:
|
|
93
|
+
base = schema_set or rules_set
|
|
94
|
+
elif mode == DefaultsMergeMode.RULES_PRIORITY:
|
|
95
|
+
base = rules_set or schema_set
|
|
96
|
+
elif mode == DefaultsMergeMode.UNION:
|
|
97
|
+
base = schema_set | rules_set
|
|
98
|
+
elif mode == DefaultsMergeMode.INTERSECTION:
|
|
99
|
+
base = schema_set & rules_set
|
|
100
|
+
else:
|
|
101
|
+
base = schema_set or rules_set
|
|
102
|
+
final = base | manual_set
|
|
103
|
+
|
|
104
|
+
self.default_fields = final
|
|
105
|
+
return self
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from typing import Dict, List, Set, Tuple
|
|
2
|
+
|
|
3
|
+
from nlbone.interfaces.api.additional_filed.field_registry import (
|
|
4
|
+
DefaultsMergeMode,
|
|
5
|
+
FieldRule,
|
|
6
|
+
PermissionChecker,
|
|
7
|
+
ResourceRegistry,
|
|
8
|
+
)
|
|
9
|
+
from nlbone.interfaces.api.dependencies.auth import client_or_user_has_access_func
|
|
10
|
+
from nlbone.interfaces.api.exceptions import BadRequestException, InternalServerException
|
|
11
|
+
|
|
12
|
+
MAX_FIELDS = 50
|
|
13
|
+
MAX_BUNDLES = 20
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AdditionalFieldsRequest:
|
|
17
|
+
"""
|
|
18
|
+
FastAPI dependency
|
|
19
|
+
- fields: ?fields=rating,costPrice,supplier.address.city
|
|
20
|
+
- bundles: ?bundles=@analytics,@internal
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
fields: str | None = None,
|
|
26
|
+
bundles: str | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
self.fields = self._parse_csv(fields)[:MAX_FIELDS]
|
|
29
|
+
self.bundles = self._parse_csv(bundles)[:MAX_BUNDLES]
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def _parse_csv(s: str | None) -> List[str]:
|
|
33
|
+
if not s:
|
|
34
|
+
return []
|
|
35
|
+
return [x.strip() for x in s.split(",") if x.strip()]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def resolve_requested_fields(
|
|
39
|
+
reg: ResourceRegistry,
|
|
40
|
+
additional_fields: AdditionalFieldsRequest = None,
|
|
41
|
+
can: PermissionChecker = None,
|
|
42
|
+
mode: DefaultsMergeMode = DefaultsMergeMode.UNION,
|
|
43
|
+
) -> Tuple[Set[str], Dict[str, FieldRule]]:
|
|
44
|
+
if not additional_fields:
|
|
45
|
+
additional_fields = AdditionalFieldsRequest()
|
|
46
|
+
if not can:
|
|
47
|
+
can = client_or_user_has_access_func
|
|
48
|
+
reg.finalize_defaults(mode=mode)
|
|
49
|
+
expanded_from_bundles = _expand_bundles(reg, set(additional_fields.bundles))
|
|
50
|
+
|
|
51
|
+
requested = set(reg.default_fields) | set(additional_fields.fields) | expanded_from_bundles
|
|
52
|
+
|
|
53
|
+
# validation
|
|
54
|
+
unknown = {f for f in requested if (f not in reg.default_fields and f not in reg.rules)}
|
|
55
|
+
if unknown:
|
|
56
|
+
raise BadRequestException(f"unknown_fields: {sorted(unknown)}")
|
|
57
|
+
|
|
58
|
+
# permission
|
|
59
|
+
selected_rules: Dict[str, FieldRule] = {}
|
|
60
|
+
for f in requested:
|
|
61
|
+
rule = reg.rules.get(f)
|
|
62
|
+
if rule and rule.permission:
|
|
63
|
+
try:
|
|
64
|
+
if not can(rule.permission):
|
|
65
|
+
continue
|
|
66
|
+
except Exception:
|
|
67
|
+
continue
|
|
68
|
+
|
|
69
|
+
if f in reg.default_fields and f not in list(reg.rules.keys()):
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if not rule.default and f not in additional_fields.fields:
|
|
73
|
+
continue
|
|
74
|
+
selected_rules[f] = rule
|
|
75
|
+
|
|
76
|
+
# dependencies
|
|
77
|
+
final = set(requested)
|
|
78
|
+
|
|
79
|
+
def add_deps(name: str):
|
|
80
|
+
if name in reg.default_fields:
|
|
81
|
+
return
|
|
82
|
+
r = reg.rules.get(name)
|
|
83
|
+
if not r:
|
|
84
|
+
return
|
|
85
|
+
for d in r.deps:
|
|
86
|
+
final.add(d)
|
|
87
|
+
add_deps(d)
|
|
88
|
+
|
|
89
|
+
for f in list(selected_rules.keys()):
|
|
90
|
+
add_deps(f)
|
|
91
|
+
|
|
92
|
+
# validate deps too
|
|
93
|
+
unknown_deps = {d for d in final if (d not in reg.default_fields and d not in reg.rules)}
|
|
94
|
+
if unknown_deps:
|
|
95
|
+
raise InternalServerException(f"registry_missing_rules_for: {sorted(unknown_deps)}")
|
|
96
|
+
|
|
97
|
+
# rules
|
|
98
|
+
return final, selected_rules
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _expand_bundles(reg: ResourceRegistry, bundles: Set[str]) -> Set[str]:
|
|
102
|
+
out: Set[str] = set()
|
|
103
|
+
seen_bundles: Set[str] = set()
|
|
104
|
+
|
|
105
|
+
def dfs(b: str):
|
|
106
|
+
if b not in reg.bundles:
|
|
107
|
+
raise BadRequestException(f"Unknown bundle: {b}")
|
|
108
|
+
if b in seen_bundles:
|
|
109
|
+
return
|
|
110
|
+
seen_bundles.add(b)
|
|
111
|
+
for item in reg.bundles[b]:
|
|
112
|
+
if item.startswith("@"):
|
|
113
|
+
dfs(item)
|
|
114
|
+
else:
|
|
115
|
+
out.add(item)
|
|
116
|
+
|
|
117
|
+
for b in bundles:
|
|
118
|
+
b = b.lstrip("@")
|
|
119
|
+
dfs(b)
|
|
120
|
+
return out
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def build_query_plan(
|
|
124
|
+
reg: ResourceRegistry,
|
|
125
|
+
selected_rules: Dict[str, FieldRule],
|
|
126
|
+
):
|
|
127
|
+
columns = []
|
|
128
|
+
joins = []
|
|
129
|
+
for r in selected_rules.values():
|
|
130
|
+
columns.extend(r.columns or [])
|
|
131
|
+
joins.extend(r.join_paths or [])
|
|
132
|
+
return columns, joins
|
|
@@ -20,14 +20,18 @@ def current_client_id() -> str:
|
|
|
20
20
|
raise UnauthorizedException()
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
def client_has_access_func(*, permissions=None):
|
|
24
|
+
request = current_request()
|
|
25
|
+
if not KeycloakAuthService().client_has_access(request.state.token, permissions=permissions):
|
|
26
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
23
30
|
def client_has_access(*, permissions=None):
|
|
24
31
|
def decorator(func):
|
|
25
32
|
@functools.wraps(func)
|
|
26
33
|
def wrapper(*args, **kwargs):
|
|
27
|
-
|
|
28
|
-
if not KeycloakAuthService().client_has_access(request.state.token, permissions=permissions):
|
|
29
|
-
raise ForbiddenException(f"Forbidden {permissions}")
|
|
30
|
-
|
|
34
|
+
client_has_access_func(permissions=permissions)
|
|
31
35
|
return func(*args, **kwargs)
|
|
32
36
|
|
|
33
37
|
return wrapper
|
|
@@ -45,18 +49,22 @@ def user_authenticated(func):
|
|
|
45
49
|
return wrapper
|
|
46
50
|
|
|
47
51
|
|
|
52
|
+
def user_has_access_func(*, permissions=None):
|
|
53
|
+
request = current_request()
|
|
54
|
+
if not current_user_id():
|
|
55
|
+
raise UnauthorizedException()
|
|
56
|
+
user_permissions = get_auth_service().get_permissions(request.state.token)
|
|
57
|
+
for p in permissions or []:
|
|
58
|
+
if p not in user_permissions:
|
|
59
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
|
|
48
63
|
def has_access(*, permissions=None):
|
|
49
64
|
def decorator(func):
|
|
50
65
|
@functools.wraps(func)
|
|
51
66
|
def wrapper(*args, **kwargs):
|
|
52
|
-
|
|
53
|
-
if not current_user_id():
|
|
54
|
-
raise UnauthorizedException()
|
|
55
|
-
user_permissions = get_auth_service().get_permissions(request.state.token)
|
|
56
|
-
for p in permissions or []:
|
|
57
|
-
if p not in user_permissions:
|
|
58
|
-
raise ForbiddenException(f"Forbidden {permissions}")
|
|
59
|
-
|
|
67
|
+
user_has_access_func(permissions=permissions)
|
|
60
68
|
return func(*args, **kwargs)
|
|
61
69
|
|
|
62
70
|
return wrapper
|
|
@@ -64,30 +72,23 @@ def has_access(*, permissions=None):
|
|
|
64
72
|
return decorator
|
|
65
73
|
|
|
66
74
|
|
|
75
|
+
def client_or_user_has_access_func(permissions=None, client_permissions=None):
|
|
76
|
+
request = current_request()
|
|
77
|
+
token = getattr(request.state, "token", None)
|
|
78
|
+
if not token:
|
|
79
|
+
raise UnauthorizedException()
|
|
80
|
+
needed = client_permissions or permissions
|
|
81
|
+
if client_has_access_func(permissions=needed):
|
|
82
|
+
return
|
|
83
|
+
if user_has_access_func(permissions=needed):
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
|
|
67
87
|
def client_or_user_has_access(*, permissions=None, client_permissions=None):
|
|
68
88
|
def decorator(func):
|
|
69
89
|
@functools.wraps(func)
|
|
70
90
|
def wrapper(*args, **kwargs):
|
|
71
|
-
|
|
72
|
-
token = getattr(request.state, "token", None)
|
|
73
|
-
if not token:
|
|
74
|
-
raise UnauthorizedException()
|
|
75
|
-
|
|
76
|
-
auth = KeycloakAuthService()
|
|
77
|
-
|
|
78
|
-
if auth.get_client_id(token):
|
|
79
|
-
needed = client_permissions or permissions
|
|
80
|
-
if not auth.client_has_access(token, permissions=needed):
|
|
81
|
-
raise ForbiddenException(f"Forbidden (client) {needed}")
|
|
82
|
-
else:
|
|
83
|
-
if not current_user_id():
|
|
84
|
-
raise UnauthorizedException()
|
|
85
|
-
|
|
86
|
-
user_permissions = get_auth_service().get_permissions(request.state.token)
|
|
87
|
-
for p in permissions or []:
|
|
88
|
-
if p not in user_permissions:
|
|
89
|
-
raise ForbiddenException(f"Forbidden {permissions}")
|
|
90
|
-
|
|
91
|
+
client_or_user_has_access_func(permissions=permissions, client_permissions=client_permissions)
|
|
91
92
|
return func(*args, **kwargs)
|
|
92
93
|
|
|
93
94
|
return wrapper
|
|
@@ -67,6 +67,14 @@ class NotSupportedException(BaseHttpException):
|
|
|
67
67
|
)
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class InternalServerException(BaseHttpException):
|
|
71
|
+
def __init__(self, detail: str = "internal_server_error"):
|
|
72
|
+
super().__init__(
|
|
73
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
74
|
+
detail=detail,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
70
78
|
class UnprocessableEntityException(BaseHttpException):
|
|
71
79
|
def __init__(self, detail: str, loc: Iterable[Any] | None = None, type_: str = "unprocessable_entity"):
|
|
72
80
|
super().__init__(
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Any, Iterable, Optional, Type, TypeVar, Union, get_args, get_origin
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, TypeAdapter
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ResponsePreference(str, Enum):
|
|
11
|
+
minimal = "minimal"
|
|
12
|
+
lite = "lite"
|
|
13
|
+
full = "full"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
M = TypeVar("M", bound=BaseModel)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AdaptiveSchemaBase(ABC):
|
|
20
|
+
"""Base for a *family* of Pydantic schemas of the same resource."""
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def minimal(cls) -> Type[M]: ...
|
|
25
|
+
@classmethod
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def lite(cls) -> Type[M]: ...
|
|
28
|
+
@classmethod
|
|
29
|
+
@abstractmethod
|
|
30
|
+
def full(cls) -> Type[M]: ...
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def choose(cls, pref: Optional[ResponsePreference]) -> Type[M]:
|
|
34
|
+
if pref is None:
|
|
35
|
+
pref = ResponsePreference.lite
|
|
36
|
+
return {
|
|
37
|
+
ResponsePreference.minimal: cls.minimal(),
|
|
38
|
+
ResponsePreference.lite: cls.lite(),
|
|
39
|
+
ResponsePreference.full: cls.full(),
|
|
40
|
+
}.get(pref, cls.lite())
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def serialize(cls, obj: Any, pref: Optional[ResponsePreference] = None) -> Any:
|
|
44
|
+
schema = cls.choose(pref)
|
|
45
|
+
adapter = TypeAdapter(schema)
|
|
46
|
+
|
|
47
|
+
if _is_iterable(obj):
|
|
48
|
+
return [cls._serialize_one(adapter, schema, x, pref) for x in obj]
|
|
49
|
+
return cls._serialize_one(adapter, schema, obj, pref)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def _serialize_one(
|
|
53
|
+
cls, adapter: TypeAdapter, schema_cls: Type[BaseModel], item: Any, pref: Optional[ResponsePreference]
|
|
54
|
+
) -> dict:
|
|
55
|
+
model = adapter.validate_python(item)
|
|
56
|
+
data = model.model_dump()
|
|
57
|
+
|
|
58
|
+
annotations = getattr(schema_cls, "__annotations__", {})
|
|
59
|
+
for field_name, annotated_type in annotations.items():
|
|
60
|
+
if field_name not in data:
|
|
61
|
+
continue
|
|
62
|
+
value = getattr(item, field_name, None)
|
|
63
|
+
if value is None:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
if _is_adaptive(annotated_type):
|
|
67
|
+
# تکشیء
|
|
68
|
+
data[field_name] = annotated_type.serialize(value, pref)
|
|
69
|
+
elif _is_list_of_adaptive(annotated_type):
|
|
70
|
+
data[field_name] = annotated_type.__args__[0].serialize(value, pref)
|
|
71
|
+
elif _is_optional_of_adaptive(annotated_type):
|
|
72
|
+
inner = _unwrap_optional(annotated_type)
|
|
73
|
+
data[field_name] = None if value is None else inner.serialize(value, pref)
|
|
74
|
+
|
|
75
|
+
return data
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# ---------- helpers ----------
|
|
79
|
+
def _is_iterable(x: Any) -> bool:
|
|
80
|
+
if isinstance(x, (str, bytes, dict)):
|
|
81
|
+
return False
|
|
82
|
+
try:
|
|
83
|
+
iter(x)
|
|
84
|
+
return True
|
|
85
|
+
except TypeError:
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _is_adaptive(t) -> bool:
|
|
90
|
+
return isinstance(t, type) and issubclass(t, AdaptiveSchemaBase)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _is_list_of_adaptive(t) -> bool:
|
|
94
|
+
origin = get_origin(t)
|
|
95
|
+
if origin not in (
|
|
96
|
+
list,
|
|
97
|
+
list.__class__,
|
|
98
|
+
Iterable,
|
|
99
|
+
):
|
|
100
|
+
return False
|
|
101
|
+
args = get_args(t)
|
|
102
|
+
return len(args) == 1 and _is_adaptive(args[0])
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_optional_of_adaptive(t) -> bool:
|
|
106
|
+
return (
|
|
107
|
+
get_origin(t) is Union
|
|
108
|
+
and len([a for a in get_args(t) if a is not type(None)]) == 1
|
|
109
|
+
and _is_adaptive([a for a in get_args(t) if a is not type(None)][0])
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _unwrap_optional(t):
|
|
114
|
+
return [a for a in get_args(t) if a is not type(None)][0]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
from pydantic import BaseModel, ConfigDict, model_serializer
|
|
3
|
+
|
|
4
|
+
EXCLUDE_NONE = "exclude_none"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseResponseModel(BaseModel):
|
|
8
|
+
model_config = ConfigDict(from_attributes=True)
|
|
9
|
+
|
|
10
|
+
@model_serializer(mode="wrap")
|
|
11
|
+
def _auto_hide_none_fields(self, handler):
|
|
12
|
+
data = handler(self)
|
|
13
|
+
|
|
14
|
+
hide_candidates = {
|
|
15
|
+
name for name, f in self.model_fields.items() if (f.json_schema_extra or {}).get(EXCLUDE_NONE) is True
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for key in list(data.keys()):
|
|
19
|
+
if key in hide_candidates and data[key] is None:
|
|
20
|
+
data.pop(key)
|
|
21
|
+
|
|
22
|
+
return data
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
from abc import ABC, abstractmethod
|
|
2
|
-
from enum import Enum
|
|
3
|
-
from typing import Any, List, Type, get_args, get_origin
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel
|
|
6
|
-
|
|
7
|
-
from nlbone.utils.context import current_request
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class ResponsePreference(str, Enum):
|
|
11
|
-
minimal = "minimal"
|
|
12
|
-
lite = "lite"
|
|
13
|
-
full = "full"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
class AdaptiveSchemaBase(ABC):
|
|
17
|
-
@classmethod
|
|
18
|
-
@abstractmethod
|
|
19
|
-
def minimal(cls) -> Type:
|
|
20
|
-
pass
|
|
21
|
-
|
|
22
|
-
@classmethod
|
|
23
|
-
@abstractmethod
|
|
24
|
-
def lite(cls) -> Type:
|
|
25
|
-
pass
|
|
26
|
-
|
|
27
|
-
@classmethod
|
|
28
|
-
@abstractmethod
|
|
29
|
-
def full(cls) -> Type:
|
|
30
|
-
pass
|
|
31
|
-
|
|
32
|
-
@classmethod
|
|
33
|
-
def choose(cls, preference: ResponsePreference) -> Type:
|
|
34
|
-
if preference == ResponsePreference.minimal:
|
|
35
|
-
return cls.minimal()
|
|
36
|
-
elif preference == ResponsePreference.lite:
|
|
37
|
-
return cls.lite()
|
|
38
|
-
elif preference == ResponsePreference.full:
|
|
39
|
-
return cls.full()
|
|
40
|
-
return cls.lite()
|
|
41
|
-
|
|
42
|
-
@classmethod
|
|
43
|
-
def serialize(cls, obj: Any, preference: ResponsePreference = None) -> Any:
|
|
44
|
-
if not preference:
|
|
45
|
-
preference = current_request().state.response_preference
|
|
46
|
-
|
|
47
|
-
schema = cls.choose(preference)
|
|
48
|
-
|
|
49
|
-
if isinstance(obj, list):
|
|
50
|
-
return [cls._recursive_serialize(schema, item) for item in obj]
|
|
51
|
-
|
|
52
|
-
return cls._recursive_serialize(schema, obj)
|
|
53
|
-
|
|
54
|
-
@classmethod
|
|
55
|
-
def _recursive_serialize(cls, schema_cls: type[BaseModel], item: Any) -> dict:
|
|
56
|
-
# Create base dict from ORM model
|
|
57
|
-
raw_data = schema_cls.model_validate(item).model_dump()
|
|
58
|
-
|
|
59
|
-
# Check each field to see if it needs nested serialization
|
|
60
|
-
annotations = schema_cls.__annotations__
|
|
61
|
-
|
|
62
|
-
for field_name, field_type in annotations.items():
|
|
63
|
-
field_value = getattr(item, field_name, None)
|
|
64
|
-
|
|
65
|
-
if field_value is None:
|
|
66
|
-
continue
|
|
67
|
-
|
|
68
|
-
# Check for nested AdaptiveSchemaBase (single object)
|
|
69
|
-
if isinstance(field_type, type) and issubclass(field_type, AdaptiveSchemaBase):
|
|
70
|
-
raw_data[field_name] = field_type.serialize(field_value)
|
|
71
|
-
|
|
72
|
-
# Check for list[AdaptiveSchemaBase]
|
|
73
|
-
elif get_origin(field_type) in (list, List):
|
|
74
|
-
sub_type = get_args(field_type)[0]
|
|
75
|
-
if isinstance(sub_type, type) and issubclass(sub_type, AdaptiveSchemaBase):
|
|
76
|
-
raw_data[field_name] = sub_type.serialize(field_value)
|
|
77
|
-
|
|
78
|
-
return raw_data
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nlbone-0.6.14 → nlbone-0.6.15}/src/nlbone/adapters/http_clients/uploadchi/uploadchi_async.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|