nlbone 0.6.13__py3-none-any.whl → 0.6.15__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.
- nlbone/adapters/auth/keycloak.py +2 -2
- nlbone/adapters/db/postgres/query_builder.py +52 -23
- nlbone/adapters/http_clients/pricing/pricing_service.py +4 -4
- nlbone/adapters/http_clients/uploadchi/uploadchi.py +15 -14
- nlbone/adapters/percolation/connection.py +3 -1
- nlbone/interfaces/api/additional_filed/__init__.py +7 -0
- nlbone/interfaces/api/additional_filed/assembler.py +54 -0
- nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py +1 -0
- nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py +13 -0
- nlbone/interfaces/api/additional_filed/field_registry.py +105 -0
- nlbone/interfaces/api/additional_filed/resolver.py +132 -0
- nlbone/interfaces/api/dependencies/auth.py +33 -32
- nlbone/interfaces/api/exceptions.py +8 -0
- nlbone/interfaces/api/pagination/__init__.py +2 -2
- nlbone/interfaces/api/pagination/offset_base.py +16 -14
- nlbone/interfaces/api/schema/__init__.py +2 -0
- nlbone/interfaces/api/schema/adaptive_schema.py +114 -0
- nlbone/interfaces/api/schema/base_response_model.py +22 -0
- nlbone/interfaces/api/schemas.py +5 -0
- {nlbone-0.6.13.dist-info → nlbone-0.6.15.dist-info}/METADATA +1 -1
- {nlbone-0.6.13.dist-info → nlbone-0.6.15.dist-info}/RECORD +24 -15
- {nlbone-0.6.13.dist-info → nlbone-0.6.15.dist-info}/WHEEL +0 -0
- {nlbone-0.6.13.dist-info → nlbone-0.6.15.dist-info}/entry_points.txt +0 -0
- {nlbone-0.6.13.dist-info → nlbone-0.6.15.dist-info}/licenses/LICENSE +0 -0
nlbone/adapters/auth/keycloak.py
CHANGED
|
@@ -36,7 +36,7 @@ class KeycloakAuthService(AuthService):
|
|
|
36
36
|
realm_name=s.KEYCLOAK_REALM_NAME,
|
|
37
37
|
client_secret_key=s.KEYCLOAK_CLIENT_SECRET.get_secret_value().strip(),
|
|
38
38
|
)
|
|
39
|
-
self.bypass = s.ENV !=
|
|
39
|
+
self.bypass = s.ENV != "prod"
|
|
40
40
|
|
|
41
41
|
def has_access(self, token, permissions):
|
|
42
42
|
if self.bypass:
|
|
@@ -145,4 +145,4 @@ class KeycloakAuthService(AuthService):
|
|
|
145
145
|
|
|
146
146
|
@functools.lru_cache(maxsize=1)
|
|
147
147
|
def get_auth_service() -> KeycloakAuthService:
|
|
148
|
-
return KeycloakAuthService()
|
|
148
|
+
return KeycloakAuthService()
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from typing import Any, Callable, Optional, Sequence, Type, Union
|
|
2
2
|
|
|
3
|
-
from sqlalchemy import
|
|
3
|
+
from sqlalchemy import and_, asc, case, desc, literal, or_
|
|
4
4
|
from sqlalchemy.dialects.postgresql import ENUM as PGEnum
|
|
5
|
-
from sqlalchemy.orm import Query, Session
|
|
5
|
+
from sqlalchemy.orm import Query, Session, aliased
|
|
6
|
+
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
6
7
|
from sqlalchemy.orm.interfaces import LoaderOption
|
|
8
|
+
from sqlalchemy.orm.relationships import RelationshipProperty
|
|
7
9
|
from sqlalchemy.sql.sqltypes import (
|
|
8
10
|
BigInteger,
|
|
9
11
|
Boolean,
|
|
@@ -28,12 +30,6 @@ class _InvalidEnum(Exception):
|
|
|
28
30
|
pass
|
|
29
31
|
|
|
30
32
|
|
|
31
|
-
from sqlalchemy.orm import aliased
|
|
32
|
-
from sqlalchemy.inspection import inspect
|
|
33
|
-
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
34
|
-
from sqlalchemy.orm.relationships import RelationshipProperty
|
|
35
|
-
|
|
36
|
-
|
|
37
33
|
def _resolve_column_and_joins(entity, query, field_path: str, join_cache: dict[str, Any]):
|
|
38
34
|
parts = [p for p in field_path.split(".") if p]
|
|
39
35
|
if not parts:
|
|
@@ -141,13 +137,15 @@ def _to_sql_like_pattern(s: str) -> str:
|
|
|
141
137
|
|
|
142
138
|
def _parse_field_and_op(field: str):
|
|
143
139
|
"""
|
|
144
|
-
Supports
|
|
145
|
-
|
|
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'}
|
|
146
143
|
"""
|
|
147
144
|
if "__" in field:
|
|
148
145
|
base, op = field.rsplit("__", 1)
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
op = op.lower()
|
|
147
|
+
if base and op in {"ilike", "gte", "lte", "lt", "gt", "ne"}:
|
|
148
|
+
return base, op
|
|
151
149
|
return field, "eq"
|
|
152
150
|
|
|
153
151
|
|
|
@@ -201,6 +199,7 @@ def _apply_filters(pagination, entity, query):
|
|
|
201
199
|
return v
|
|
202
200
|
|
|
203
201
|
try:
|
|
202
|
+
|
|
204
203
|
def _use_ilike(v) -> bool:
|
|
205
204
|
if op_hint == "ilike":
|
|
206
205
|
return True
|
|
@@ -217,10 +216,24 @@ def _apply_filters(pagination, entity, query):
|
|
|
217
216
|
patterns = [_to_sql_like_pattern(str(v)) for v in vals]
|
|
218
217
|
predicates.append(or_(*[col.ilike(p) for p in patterns]))
|
|
219
218
|
else:
|
|
220
|
-
coerced = [coerce(v) for v in vals]
|
|
219
|
+
coerced = [coerce(v) for v in vals if v is not None]
|
|
221
220
|
if not coerced:
|
|
222
221
|
continue
|
|
223
|
-
|
|
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))
|
|
224
237
|
else:
|
|
225
238
|
if _use_ilike(value) and _is_text_type(coltype):
|
|
226
239
|
pattern = _to_sql_like_pattern(str(value))
|
|
@@ -228,9 +241,25 @@ def _apply_filters(pagination, entity, query):
|
|
|
228
241
|
else:
|
|
229
242
|
v = coerce(value)
|
|
230
243
|
if v is None:
|
|
231
|
-
|
|
244
|
+
if op_hint in {"eq", "ilike"}:
|
|
245
|
+
predicates.append(col.is_(None))
|
|
246
|
+
else:
|
|
247
|
+
continue
|
|
232
248
|
else:
|
|
233
|
-
|
|
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)
|
|
234
263
|
|
|
235
264
|
except _InvalidEnum as e:
|
|
236
265
|
raise UnprocessableEntityException(str(e), loc=["query", "filters", raw_field]) from e
|
|
@@ -304,13 +333,13 @@ def _serialize_item(item: Any, output_cls: OutputType) -> Any:
|
|
|
304
333
|
|
|
305
334
|
|
|
306
335
|
def get_paginated_response(
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
336
|
+
pagination,
|
|
337
|
+
entity,
|
|
338
|
+
session: Session,
|
|
339
|
+
*,
|
|
340
|
+
with_count: bool = True,
|
|
341
|
+
output_cls: Optional[Type] = None,
|
|
342
|
+
eager_options: Optional[Sequence[LoaderOption]] = None,
|
|
314
343
|
) -> dict:
|
|
315
344
|
query = session.query(entity)
|
|
316
345
|
if eager_options:
|
|
@@ -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):
|
|
@@ -35,11 +35,11 @@ def _filename_from_cd(cd: str | None, fallback: str) -> str:
|
|
|
35
35
|
|
|
36
36
|
class UploadchiClient(FileServicePort):
|
|
37
37
|
def __init__(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
self,
|
|
39
|
+
token_provider: ClientTokenProvider | None = None,
|
|
40
|
+
base_url: Optional[str] = None,
|
|
41
|
+
timeout_seconds: Optional[float] = None,
|
|
42
|
+
client: httpx.Client | None = None,
|
|
43
43
|
) -> None:
|
|
44
44
|
s = get_settings()
|
|
45
45
|
self._base_url = normalize_https_base(base_url or str(s.UPLOADCHI_BASE_URL))
|
|
@@ -51,7 +51,7 @@ class UploadchiClient(FileServicePort):
|
|
|
51
51
|
self._client.close()
|
|
52
52
|
|
|
53
53
|
def upload_file(
|
|
54
|
-
|
|
54
|
+
self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
|
|
55
55
|
) -> dict:
|
|
56
56
|
tok = _resolve_token(token)
|
|
57
57
|
files = {"file": (filename, file_bytes)}
|
|
@@ -84,12 +84,12 @@ class UploadchiClient(FileServicePort):
|
|
|
84
84
|
raise UploadchiError(r.status_code, r.text)
|
|
85
85
|
|
|
86
86
|
def list_files(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
self,
|
|
88
|
+
limit: int = 10,
|
|
89
|
+
offset: int = 0,
|
|
90
|
+
filters: dict[str, Any] | None = None,
|
|
91
|
+
sort: list[tuple[str, str]] | None = None,
|
|
92
|
+
token: str | None = None,
|
|
93
93
|
) -> dict:
|
|
94
94
|
tok = _resolve_token(token)
|
|
95
95
|
q = build_list_query(limit, offset, filters, sort)
|
|
@@ -116,7 +116,8 @@ class UploadchiClient(FileServicePort):
|
|
|
116
116
|
|
|
117
117
|
def delete_file(self, file_id: str, token: str | None = None) -> None:
|
|
118
118
|
tok = _resolve_token(token)
|
|
119
|
-
r = self._client.delete(
|
|
120
|
-
|
|
119
|
+
r = self._client.delete(
|
|
120
|
+
f"{self._base_url}/{file_id}", headers=auth_headers(tok or self._token_provider.get_access_token())
|
|
121
|
+
)
|
|
121
122
|
if r.status_code not in (204, 200):
|
|
122
123
|
raise UploadchiError(r.status_code, r.text)
|
|
@@ -9,6 +9,8 @@ def get_es_client():
|
|
|
9
9
|
es = Elasticsearch(
|
|
10
10
|
setting.ELASTIC_PERCOLATE_URL,
|
|
11
11
|
basic_auth=(setting.ELASTIC_PERCOLATE_USER, setting.ELASTIC_PERCOLATE_PASS.get_secret_value().strip()),
|
|
12
|
-
http_compress=True,
|
|
12
|
+
http_compress=True,
|
|
13
|
+
max_retries=2,
|
|
14
|
+
retry_on_timeout=True,
|
|
13
15
|
)
|
|
14
16
|
return es
|
|
@@ -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
|
|
@@ -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__(
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from math import ceil
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, List, Optional
|
|
4
4
|
|
|
5
5
|
from fastapi import Query
|
|
6
6
|
|
|
@@ -13,18 +13,20 @@ class PaginateRequest:
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
def __init__(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
self,
|
|
17
|
+
limit: int = 10,
|
|
18
|
+
offset: int = 0,
|
|
19
|
+
sort: Optional[str] = None,
|
|
20
|
+
filters: Optional[str] = Query(None, description="e.g. title:abc"),
|
|
21
|
+
include: Optional[str] = None,
|
|
22
22
|
) -> None:
|
|
23
23
|
self.limit = max(0, limit)
|
|
24
24
|
self.offset = max(0, offset)
|
|
25
25
|
self.sort = self._parse_sort(sort)
|
|
26
26
|
self.filters = self._parse_filters(filters or "")
|
|
27
|
-
self.include_ids: List[int] = ([int(x) for x in include.split(",") if x.strip().isdigit()] if include else [])[
|
|
27
|
+
self.include_ids: List[int] = ([int(x) for x in include.split(",") if x.strip().isdigit()] if include else [])[
|
|
28
|
+
:50
|
|
29
|
+
]
|
|
28
30
|
|
|
29
31
|
@staticmethod
|
|
30
32
|
def _parse_sort(sort_str: Optional[str]) -> list[dict[str, str]]:
|
|
@@ -83,12 +85,12 @@ class PaginateResponse:
|
|
|
83
85
|
"""
|
|
84
86
|
|
|
85
87
|
def __init__(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
self,
|
|
89
|
+
data: list[Any],
|
|
90
|
+
total_count: int | None,
|
|
91
|
+
limit: int,
|
|
92
|
+
offset: int,
|
|
93
|
+
use_data_key: bool = True,
|
|
92
94
|
) -> None:
|
|
93
95
|
self.data = data
|
|
94
96
|
self.total_count = total_count
|
|
@@ -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
|
nlbone/interfaces/api/schemas.py
CHANGED
|
@@ -3,7 +3,7 @@ nlbone/container.py,sha256=rVYzH-jIM8iCcefDOo29mNjvFdf3nJ4EtPNUws9SDnA,3089
|
|
|
3
3
|
nlbone/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
nlbone/adapters/__init__.py,sha256=NzUmk4XPyp3GJOw7VSE86xkQMZLtG3MrOoXLeoB551M,41
|
|
5
5
|
nlbone/adapters/auth/__init__.py,sha256=hkDHvsFhw_UiOHG9ZSMqjiAhK4wumEforitveSZswVw,42
|
|
6
|
-
nlbone/adapters/auth/keycloak.py,sha256=
|
|
6
|
+
nlbone/adapters/auth/keycloak.py,sha256=IhEriaFl5mjIGT6ZUCU9qROd678ARchvWgd4UJ6zH7s,4925
|
|
7
7
|
nlbone/adapters/auth/token_provider.py,sha256=vL2Hk6HXnBbpk40Tq1wpqak5QQ7KEQf3nRquT0N8V4Q,1433
|
|
8
8
|
nlbone/adapters/cache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
nlbone/adapters/cache/async_redis.py,sha256=vvu5w4ANx0BVRHL95RAMGsD8CcaC-tSBMbCius2cuNc,6212
|
|
@@ -15,7 +15,7 @@ nlbone/adapters/db/postgres/__init__.py,sha256=6JYJH0xZs3aR-zuyMpRhsdzFugmqz8npr
|
|
|
15
15
|
nlbone/adapters/db/postgres/audit.py,sha256=8f5XOuW7_ybJyy_STam1FNzqmZAAVAu7tmMRUkCGJOM,4594
|
|
16
16
|
nlbone/adapters/db/postgres/base.py,sha256=kha9xmklzhuQAK8QEkNBn-mAHq8dUKbOM-3abaBpWmQ,71
|
|
17
17
|
nlbone/adapters/db/postgres/engine.py,sha256=UCegauVB1gvo42ThytYnn5VIcQBwR-5xhcXYFApRFNk,3448
|
|
18
|
-
nlbone/adapters/db/postgres/query_builder.py,sha256=
|
|
18
|
+
nlbone/adapters/db/postgres/query_builder.py,sha256=V-Eb98xi9ajhJaEAW1BMtZot9A3muZa5gO_NRhYR0bQ,12505
|
|
19
19
|
nlbone/adapters/db/postgres/repository.py,sha256=J_DBE73JhHPYCk90c5-O7lQtZbxDgqjjN9OcWy4Omvs,1660
|
|
20
20
|
nlbone/adapters/db/postgres/schema.py,sha256=NlE7Rr8uXypsw4oWkdZhZwcIBHQEPIpoHLxcUo98i6s,1039
|
|
21
21
|
nlbone/adapters/db/postgres/uow.py,sha256=nRxNpY-WoWHpym-XeZ8VHm0MYvtB9wuopOeNdV_ebk8,2088
|
|
@@ -23,15 +23,15 @@ nlbone/adapters/db/redis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
23
23
|
nlbone/adapters/db/redis/client.py,sha256=5SUnwP2-GrueSFimUbiqDvrQsumvIE2aeozk8l-vOfQ,466
|
|
24
24
|
nlbone/adapters/http_clients/__init__.py,sha256=w-Yr9CLuXMU71N0Ada5HbvP1DB53wqeP6B-i5rALlTo,150
|
|
25
25
|
nlbone/adapters/http_clients/pricing/__init__.py,sha256=ElA9NFcAR9u4cqb_w3PPqKU3xGeyjNLQ8veJ0ql2iz0,81
|
|
26
|
-
nlbone/adapters/http_clients/pricing/pricing_service.py,sha256=
|
|
26
|
+
nlbone/adapters/http_clients/pricing/pricing_service.py,sha256=fYfMQh3qW_YDNkBW19RkDkIet7zUDzb2HsIT5dr1Q7Y,3319
|
|
27
27
|
nlbone/adapters/http_clients/uploadchi/__init__.py,sha256=uBzEOuVtY22teWW2b36Pitkdk5yVdSqa6xbg22JfTNg,105
|
|
28
|
-
nlbone/adapters/http_clients/uploadchi/uploadchi.py,sha256=
|
|
28
|
+
nlbone/adapters/http_clients/uploadchi/uploadchi.py,sha256=erpjOees25FW0nuK1PkYS-oU0h8MeRV9Rhs1cf3gaEs,4881
|
|
29
29
|
nlbone/adapters/http_clients/uploadchi/uploadchi_async.py,sha256=PQbVNeaYde5CmgT3vcnQoI1PGeSs9AxHlPFuB8biOmU,4717
|
|
30
30
|
nlbone/adapters/messaging/__init__.py,sha256=UDAwu3s-JQmOZjWz2Nu0SgHhnkbeOhKDH_zLD75oWMY,40
|
|
31
31
|
nlbone/adapters/messaging/event_bus.py,sha256=w-NPwDiPMLFPU_enRQCtfQXOALsXfg31u57R8sG_-1U,781
|
|
32
32
|
nlbone/adapters/messaging/redis.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
33
33
|
nlbone/adapters/percolation/__init__.py,sha256=0h1Bw7FzxgkDIHxeoyQXSfegrhP6VbpYV4QC8njYdRE,38
|
|
34
|
-
nlbone/adapters/percolation/connection.py,sha256=
|
|
34
|
+
nlbone/adapters/percolation/connection.py,sha256=1iJISSwMEh4r_6nJI7mYf_v64Q0eeU1eSI0wLIfOK14,415
|
|
35
35
|
nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
36
|
nlbone/config/logging.py,sha256=Ot6Ctf7EQZlW8YNB-uBdleqI6wixn5fH0Eo6QRgNkQk,4358
|
|
37
37
|
nlbone/config/settings.py,sha256=W3NHZP6yjIyyKiGWNkjlUt_RYFKkcIfMBoKih_z_0Bs,3911
|
|
@@ -57,20 +57,29 @@ nlbone/core/ports/uow.py,sha256=SmBdRf0NvSdIjQ3Le1QGz8kNGBk7jgNHtNguvXRwmgs,557
|
|
|
57
57
|
nlbone/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
58
58
|
nlbone/interfaces/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
59
59
|
nlbone/interfaces/api/exception_handlers.py,sha256=vxNEBgAaQygLgAz1UNt3wHj0SdCJOwtLOv_BwTfir3o,4050
|
|
60
|
-
nlbone/interfaces/api/exceptions.py,sha256=
|
|
60
|
+
nlbone/interfaces/api/exceptions.py,sha256=mvpfTf_CUy1V3Y1iGLMGJh_yWfNlJs_ubrYtJmpNW7o,2499
|
|
61
61
|
nlbone/interfaces/api/routers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
62
|
-
nlbone/interfaces/api/schemas.py,sha256=
|
|
62
|
+
nlbone/interfaces/api/schemas.py,sha256=NIEKeTdJtwwIkIxL7WURNZF8g34I4TlRAqs-x1Uq7YI,108
|
|
63
|
+
nlbone/interfaces/api/additional_filed/__init__.py,sha256=BWemliLSQV9iq1vdUaF733q0FOSipSWBOQk9eYj732Q,318
|
|
64
|
+
nlbone/interfaces/api/additional_filed/assembler.py,sha256=MyB6YimAAzMe5WUZtHei7a4ITsOpYhrDWIWNAKq7tY8,1599
|
|
65
|
+
nlbone/interfaces/api/additional_filed/field_registry.py,sha256=ScpMsjDBkuUdh_FaBmk1EXBeydERcNthyom1tcJK3BM,3820
|
|
66
|
+
nlbone/interfaces/api/additional_filed/resolver.py,sha256=eVBPj6WsUkZbM3wGPyexwA6d5GThCSgbwNzzHImaOyU,3767
|
|
67
|
+
nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py,sha256=LUSAOO3xRUt5ptlraIx7H-7dSkdr1D-WprmnqXRB16g,48
|
|
68
|
+
nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py,sha256=ecKqPeXZ-YiF14RK9PmK7ln3PCzpCUc18S5zm5IF3fw,339
|
|
63
69
|
nlbone/interfaces/api/dependencies/__init__.py,sha256=rnYRrFVZCfICQrp_PVFlzNg3BeC57yM08wn2DbOHCfk,359
|
|
64
70
|
nlbone/interfaces/api/dependencies/async_auth.py,sha256=bfxgBXhp29WqevjTG4jrdPNR-75APm4jKyHdOOtxnp4,1825
|
|
65
|
-
nlbone/interfaces/api/dependencies/auth.py,sha256=
|
|
71
|
+
nlbone/interfaces/api/dependencies/auth.py,sha256=SVqdH78ek65MdT-81_tJo9QiEbnERAh9dR10pRdTDb8,2833
|
|
66
72
|
nlbone/interfaces/api/dependencies/db.py,sha256=-UD39J_86UU7ZJs2ZncpdND0yhAG0NeeeALrgSDuuFw,466
|
|
67
73
|
nlbone/interfaces/api/dependencies/uow.py,sha256=QfLEvLYLNWZJQN1k-0q0hBVtUld3D75P4j39q_RjcnE,1181
|
|
68
74
|
nlbone/interfaces/api/middleware/__init__.py,sha256=zbX2vaEAfxRMIYwO2MVY_2O6bqG5H9o7HqGpX14U3Is,158
|
|
69
75
|
nlbone/interfaces/api/middleware/access_log.py,sha256=vIkxxxfy2HcjqqKb8XCfGCcSrivAC8u6ie75FMq5x-U,1032
|
|
70
76
|
nlbone/interfaces/api/middleware/add_request_context.py,sha256=av-qs0biOYuF9R6RJOo2eYsFqDL9WRYWcjVakFhbt-w,1834
|
|
71
77
|
nlbone/interfaces/api/middleware/authentication.py,sha256=ze7vCm492QsX9nPL6A-PqZCmC1C5ZgUE-OWI6fCLpsU,1809
|
|
72
|
-
nlbone/interfaces/api/pagination/__init__.py,sha256=
|
|
73
|
-
nlbone/interfaces/api/pagination/offset_base.py,sha256=
|
|
78
|
+
nlbone/interfaces/api/pagination/__init__.py,sha256=pA1uC4rK6eqDI5IkLVxmgO2B6lExnOm8Pje2-hifJZw,431
|
|
79
|
+
nlbone/interfaces/api/pagination/offset_base.py,sha256=iipO09sAUb0U9ctMMujBHQo1IgMRbasvwnHJFtA9ZeA,3812
|
|
80
|
+
nlbone/interfaces/api/schema/__init__.py,sha256=LAqgynfupeqOQ6u0I5ucrcYnojRMZUg9yW8IjKSQTNI,119
|
|
81
|
+
nlbone/interfaces/api/schema/adaptive_schema.py,sha256=bdWBNpP2NfOJ_in4btXn0lrZOK70x-OqfmZ-NpIJdoQ,3347
|
|
82
|
+
nlbone/interfaces/api/schema/base_response_model.py,sha256=Y-mqNzVheTaGkM4m2DsybK-GLhfODMhBUNyTRoaf_Zc,600
|
|
74
83
|
nlbone/interfaces/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
84
|
nlbone/interfaces/cli/init_db.py,sha256=C67n2MsJ1vzkJxC8zfUYOxFdd6mEB_vT9agxN6jWoG8,790
|
|
76
85
|
nlbone/interfaces/cli/main.py,sha256=pNldsTgplVyXa-Hx96dySO2I9gFRi49nDXv7J_dO73s,686
|
|
@@ -84,8 +93,8 @@ nlbone/utils/context.py,sha256=MmclJ24BG2uvSTg1IK7J-Da9BhVFDQ5ag4Ggs2FF1_w,1600
|
|
|
84
93
|
nlbone/utils/http.py,sha256=UXUoXgQdTRNT08ho8zl-C5ekfDsD8uf-JiMQ323ooqw,872
|
|
85
94
|
nlbone/utils/redactor.py,sha256=-V4HrHmHwPi3Kez587Ek1uJlgK35qGSrwBOvcbw8Jas,1279
|
|
86
95
|
nlbone/utils/time.py,sha256=DjjyQ9GLsfXoT6NK8RDW2rOlJg3e6sF04Jw6PBUrSvg,1268
|
|
87
|
-
nlbone-0.6.
|
|
88
|
-
nlbone-0.6.
|
|
89
|
-
nlbone-0.6.
|
|
90
|
-
nlbone-0.6.
|
|
91
|
-
nlbone-0.6.
|
|
96
|
+
nlbone-0.6.15.dist-info/METADATA,sha256=t9hCCQwgtmGzXZOwsvXsc5hinAX5ogPmN36zOQPdyfk,2228
|
|
97
|
+
nlbone-0.6.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
98
|
+
nlbone-0.6.15.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
|
|
99
|
+
nlbone-0.6.15.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
100
|
+
nlbone-0.6.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|