nlbone 0.6.0__py3-none-any.whl → 0.6.9__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/__init__.py +1 -0
- nlbone/adapters/auth/keycloak.py +1 -1
- nlbone/adapters/auth/token_provider.py +1 -1
- nlbone/adapters/cache/async_redis.py +18 -8
- nlbone/adapters/cache/memory.py +21 -11
- nlbone/adapters/cache/pubsub_listener.py +3 -0
- nlbone/adapters/cache/redis.py +23 -8
- nlbone/adapters/db/__init__.py +0 -1
- nlbone/adapters/db/postgres/audit.py +14 -11
- nlbone/adapters/db/postgres/query_builder.py +103 -86
- nlbone/adapters/db/redis/client.py +1 -4
- nlbone/adapters/http_clients/__init__.py +2 -2
- nlbone/adapters/http_clients/pricing/__init__.py +1 -1
- nlbone/adapters/http_clients/pricing/pricing_service.py +40 -20
- nlbone/adapters/http_clients/uploadchi/__init__.py +1 -1
- nlbone/adapters/http_clients/uploadchi/uploadchi.py +12 -12
- nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +14 -15
- nlbone/adapters/percolation/__init__.py +1 -1
- nlbone/adapters/percolation/connection.py +2 -1
- nlbone/config/logging.py +54 -24
- nlbone/container.py +14 -9
- nlbone/core/application/base_worker.py +1 -1
- nlbone/core/domain/models.py +4 -2
- nlbone/core/ports/cache.py +25 -9
- nlbone/interfaces/api/dependencies/auth.py +26 -0
- nlbone/interfaces/api/pagination/offset_base.py +14 -12
- nlbone/interfaces/cli/init_db.py +1 -1
- nlbone/interfaces/cli/main.py +6 -5
- nlbone/utils/cache.py +10 -0
- nlbone/utils/cache_keys.py +6 -0
- nlbone/utils/cache_registry.py +5 -2
- nlbone/utils/http.py +1 -1
- nlbone/utils/redactor.py +2 -1
- nlbone/utils/time.py +1 -1
- {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/METADATA +1 -1
- {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/RECORD +39 -39
- {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/WHEEL +0 -0
- {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/entry_points.txt +0 -0
- {nlbone-0.6.0.dist-info → nlbone-0.6.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
1
2
|
from enum import Enum
|
|
2
|
-
from typing import
|
|
3
|
+
from typing import Dict, List, Literal, Optional, Union
|
|
3
4
|
|
|
4
5
|
import httpx
|
|
5
6
|
import requests
|
|
@@ -7,7 +8,7 @@ from pydantic import BaseModel, Field, NonNegativeInt, RootModel
|
|
|
7
8
|
|
|
8
9
|
from nlbone.adapters.auth.token_provider import ClientTokenProvider
|
|
9
10
|
from nlbone.config.settings import get_settings
|
|
10
|
-
from nlbone.utils.http import
|
|
11
|
+
from nlbone.utils.http import auth_headers, normalize_https_base
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class PricingError(Exception):
|
|
@@ -32,11 +33,18 @@ class Product(BaseModel):
|
|
|
32
33
|
|
|
33
34
|
|
|
34
35
|
class Pricing(BaseModel):
|
|
35
|
-
source: Literal["formula", "static"]
|
|
36
|
-
price: float
|
|
36
|
+
source: Optional[Literal["formula", "static"]] = None
|
|
37
|
+
price: Optional[float] = None
|
|
37
38
|
discount: Optional[float] = None
|
|
38
39
|
discount_type: Optional[DiscountType] = None
|
|
39
|
-
params: dict
|
|
40
|
+
params: Optional[dict] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Segment(BaseModel):
|
|
44
|
+
id: str
|
|
45
|
+
name: str
|
|
46
|
+
specificity: int
|
|
47
|
+
matched_fields: list
|
|
40
48
|
|
|
41
49
|
|
|
42
50
|
class Formula(BaseModel):
|
|
@@ -49,24 +57,22 @@ class Formula(BaseModel):
|
|
|
49
57
|
|
|
50
58
|
class PricingRule(BaseModel):
|
|
51
59
|
product: Product
|
|
52
|
-
|
|
60
|
+
segment: Segment | None
|
|
53
61
|
formula: Optional[Formula] = None
|
|
54
|
-
specificity: int
|
|
55
|
-
matched_fields: list
|
|
56
62
|
pricing: Pricing
|
|
57
63
|
|
|
58
64
|
|
|
59
|
-
class CalculatePriceOut(RootModel[List[PricingRule]]):
|
|
65
|
+
class CalculatePriceOut(RootModel[Union[List[PricingRule], Dict[str, PricingRule]]]):
|
|
60
66
|
pass
|
|
61
67
|
|
|
62
68
|
|
|
63
69
|
class PricingService:
|
|
64
70
|
def __init__(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
self,
|
|
72
|
+
token_provider: ClientTokenProvider,
|
|
73
|
+
base_url: Optional[str] = None,
|
|
74
|
+
timeout_seconds: Optional[float] = None,
|
|
75
|
+
client: httpx.Client | None = None,
|
|
70
76
|
) -> None:
|
|
71
77
|
s = get_settings()
|
|
72
78
|
self._base_url = normalize_https_base(base_url or str(s.PRICING_SERVICE_URL), enforce_https=False)
|
|
@@ -74,17 +80,16 @@ class PricingService:
|
|
|
74
80
|
self._client = client or requests.session()
|
|
75
81
|
self._token_provider = token_provider
|
|
76
82
|
|
|
77
|
-
def calculate(self, items: list[CalculatePriceIn]) -> CalculatePriceOut:
|
|
78
|
-
body = {
|
|
79
|
-
"items": [i.model_dump() for i in items]
|
|
80
|
-
}
|
|
83
|
+
def calculate(self, items: list[CalculatePriceIn], response: Literal["list", "dict"] = "dict") -> CalculatePriceOut:
|
|
84
|
+
body = {"items": [i.model_dump() for i in items]}
|
|
81
85
|
|
|
82
86
|
r = self._client.post(
|
|
83
|
-
f"{self._base_url}/
|
|
87
|
+
f"{self._base_url}/price/calculate",
|
|
88
|
+
params={"response": response},
|
|
84
89
|
headers=auth_headers(self._token_provider.get_access_token()),
|
|
85
90
|
json=body,
|
|
86
91
|
timeout=self._timeout,
|
|
87
|
-
verify=False
|
|
92
|
+
verify=False,
|
|
88
93
|
)
|
|
89
94
|
|
|
90
95
|
if r.status_code not in (200, 204):
|
|
@@ -94,3 +99,18 @@ class PricingService:
|
|
|
94
99
|
return CalculatePriceOut.model_validate(root=[])
|
|
95
100
|
|
|
96
101
|
return CalculatePriceOut.model_validate(r.json())
|
|
102
|
+
|
|
103
|
+
def exchange_rates(self) -> Dict[str, Decimal]:
|
|
104
|
+
r = self._client.get(
|
|
105
|
+
f"{self._base_url}/variables/key/exchange_rates",
|
|
106
|
+
headers=auth_headers(self._token_provider.get_access_token()),
|
|
107
|
+
timeout=self._timeout,
|
|
108
|
+
verify=False,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
if r.status_code != 200:
|
|
112
|
+
raise PricingError(r.status_code, r.text)
|
|
113
|
+
|
|
114
|
+
values = r.json().get("values")
|
|
115
|
+
|
|
116
|
+
return {f"{value['key']}": Decimal(value["value"]) for value in values}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
from .uploadchi import UploadchiClient, UploadchiError
|
|
2
|
-
from .uploadchi_async import UploadchiAsyncClient
|
|
2
|
+
from .uploadchi_async import UploadchiAsyncClient
|
|
@@ -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)
|
|
@@ -4,21 +4,20 @@ from typing import Any, AsyncIterator, Optional
|
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
7
|
+
from nlbone.adapters.auth.token_provider import ClientTokenProvider
|
|
8
|
+
from nlbone.adapters.http_clients.uploadchi.uploadchi import UploadchiError, _filename_from_cd, _resolve_token
|
|
7
9
|
from nlbone.config.settings import get_settings
|
|
8
10
|
from nlbone.core.ports.files import AsyncFileServicePort
|
|
9
|
-
|
|
10
|
-
from nlbone.adapters.http_clients.uploadchi.uploadchi import UploadchiError, _filename_from_cd, _resolve_token
|
|
11
|
-
from nlbone.adapters.auth.token_provider import ClientTokenProvider
|
|
12
11
|
from nlbone.utils.http import auth_headers, build_list_query
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class UploadchiAsyncClient(AsyncFileServicePort):
|
|
16
15
|
def __init__(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
self,
|
|
17
|
+
token_provider: ClientTokenProvider | None = None,
|
|
18
|
+
base_url: Optional[str] = None,
|
|
19
|
+
timeout_seconds: Optional[float] = None,
|
|
20
|
+
client: httpx.AsyncClient | None = None,
|
|
22
21
|
) -> None:
|
|
23
22
|
s = get_settings()
|
|
24
23
|
self._base_url = base_url or str(s.UPLOADCHI_BASE_URL)
|
|
@@ -32,7 +31,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
|
|
|
32
31
|
await self._client.aclose()
|
|
33
32
|
|
|
34
33
|
async def upload_file(
|
|
35
|
-
|
|
34
|
+
self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
|
|
36
35
|
) -> dict:
|
|
37
36
|
tok = _resolve_token(token)
|
|
38
37
|
files = {"file": (filename, file_bytes)}
|
|
@@ -63,12 +62,12 @@ class UploadchiAsyncClient(AsyncFileServicePort):
|
|
|
63
62
|
raise UploadchiError(r.status_code, await r.aread())
|
|
64
63
|
|
|
65
64
|
async def list_files(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
65
|
+
self,
|
|
66
|
+
limit: int = 10,
|
|
67
|
+
offset: int = 0,
|
|
68
|
+
filters: dict[str, Any] | None = None,
|
|
69
|
+
sort: list[tuple[str, str]] | None = None,
|
|
70
|
+
token: str | None = None,
|
|
72
71
|
) -> dict:
|
|
73
72
|
tok = _resolve_token(token)
|
|
74
73
|
q = build_list_query(limit, offset, filters, sort)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
from .connection import get_es_client
|
|
1
|
+
from .connection import get_es_client
|
|
@@ -4,9 +4,10 @@ from nlbone.config.settings import get_settings
|
|
|
4
4
|
|
|
5
5
|
setting = get_settings()
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
def get_es_client():
|
|
8
9
|
es = Elasticsearch(
|
|
9
10
|
setting.ELASTIC_PERCOLATE_URL,
|
|
10
|
-
basic_auth=(setting.ELASTIC_PERCOLATE_USER, setting.ELASTIC_PERCOLATE_PASS.get_secret_value().strip())
|
|
11
|
+
basic_auth=(setting.ELASTIC_PERCOLATE_USER, setting.ELASTIC_PERCOLATE_PASS.get_secret_value().strip()),
|
|
11
12
|
)
|
|
12
13
|
return es
|
nlbone/config/logging.py
CHANGED
|
@@ -11,6 +11,7 @@ from nlbone.utils.redactor import PiiRedactor
|
|
|
11
11
|
|
|
12
12
|
settings = get_settings()
|
|
13
13
|
|
|
14
|
+
|
|
14
15
|
# ---------- Filters ----------
|
|
15
16
|
class ContextFilter(logging.Filter):
|
|
16
17
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
@@ -21,12 +22,32 @@ class ContextFilter(logging.Filter):
|
|
|
21
22
|
record.user_agent = ctx.get("user_agent")
|
|
22
23
|
return True
|
|
23
24
|
|
|
25
|
+
|
|
24
26
|
# ---------- Formatter ----------
|
|
25
27
|
class JsonFormatter(logging.Formatter):
|
|
26
28
|
RESERVED = {
|
|
27
|
-
"args",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
29
|
+
"args",
|
|
30
|
+
"asctime",
|
|
31
|
+
"created",
|
|
32
|
+
"exc_info",
|
|
33
|
+
"exc_text",
|
|
34
|
+
"filename",
|
|
35
|
+
"funcName",
|
|
36
|
+
"levelname",
|
|
37
|
+
"levelno",
|
|
38
|
+
"lineno",
|
|
39
|
+
"module",
|
|
40
|
+
"msecs",
|
|
41
|
+
"message",
|
|
42
|
+
"msg",
|
|
43
|
+
"name",
|
|
44
|
+
"pathname",
|
|
45
|
+
"process",
|
|
46
|
+
"processName",
|
|
47
|
+
"relativeCreated",
|
|
48
|
+
"stack_info",
|
|
49
|
+
"thread",
|
|
50
|
+
"threadName",
|
|
30
51
|
}
|
|
31
52
|
|
|
32
53
|
def format(self, record: logging.LogRecord) -> str:
|
|
@@ -53,17 +74,23 @@ class JsonFormatter(logging.Formatter):
|
|
|
53
74
|
|
|
54
75
|
return json.dumps(payload, ensure_ascii=False)
|
|
55
76
|
|
|
77
|
+
|
|
56
78
|
class PlainFormatter(logging.Formatter):
|
|
57
79
|
def __init__(self):
|
|
58
80
|
super().__init__(
|
|
59
|
-
fmt="%(asctime)s | %(levelname)s | %(name)s | "
|
|
60
|
-
"req=%(request_id)s user=%(user_id)s ip=%(ip)s | %(message)s",
|
|
81
|
+
fmt="%(asctime)s | %(levelname)s | %(name)s | req=%(request_id)s user=%(user_id)s ip=%(ip)s | %(message)s",
|
|
61
82
|
datefmt="%Y-%m-%dT%H:%M:%S%z",
|
|
62
83
|
)
|
|
63
84
|
|
|
85
|
+
|
|
64
86
|
# ---------- Setup ----------
|
|
65
|
-
def setup_logging(
|
|
66
|
-
|
|
87
|
+
def setup_logging(
|
|
88
|
+
*,
|
|
89
|
+
log_json: bool = settings.LOG_JSON,
|
|
90
|
+
log_level: str = settings.LOG_LEVEL,
|
|
91
|
+
log_file: str | None = None,
|
|
92
|
+
silence_uvicorn_access: bool = True,
|
|
93
|
+
):
|
|
67
94
|
handlers = {
|
|
68
95
|
"console": {
|
|
69
96
|
"class": "logging.StreamHandler",
|
|
@@ -82,23 +109,25 @@ def setup_logging(*, log_json: bool=settings.LOG_JSON, log_level: str =settings.
|
|
|
82
109
|
"formatter": "json" if log_json else "plain",
|
|
83
110
|
}
|
|
84
111
|
|
|
85
|
-
dictConfig(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
112
|
+
dictConfig(
|
|
113
|
+
{
|
|
114
|
+
"version": 1,
|
|
115
|
+
"disable_existing_loggers": False,
|
|
116
|
+
"filters": {
|
|
117
|
+
"ctx": {"()": ContextFilter},
|
|
118
|
+
"pii": {"()": PiiRedactor},
|
|
119
|
+
},
|
|
120
|
+
"formatters": {
|
|
121
|
+
"json": {"()": JsonFormatter},
|
|
122
|
+
"plain": {"()": PlainFormatter},
|
|
123
|
+
},
|
|
124
|
+
"handlers": handlers,
|
|
125
|
+
"root": {
|
|
126
|
+
"level": log_level,
|
|
127
|
+
"handlers": list(handlers.keys()),
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
)
|
|
102
131
|
|
|
103
132
|
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
104
133
|
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
@@ -115,5 +144,6 @@ def setup_logging(*, log_json: bool=settings.LOG_JSON, log_level: str =settings.
|
|
|
115
144
|
uvicorn_access.handlers = []
|
|
116
145
|
uvicorn_access.propagate = True
|
|
117
146
|
|
|
147
|
+
|
|
118
148
|
def get_logger(name: str | None = None) -> logging.Logger:
|
|
119
149
|
return logging.getLogger(name or "app")
|
nlbone/container.py
CHANGED
|
@@ -15,8 +15,9 @@ from nlbone.adapters.http_clients import PricingService
|
|
|
15
15
|
from nlbone.adapters.http_clients.uploadchi import UploadchiClient
|
|
16
16
|
from nlbone.adapters.http_clients.uploadchi.uploadchi_async import UploadchiAsyncClient
|
|
17
17
|
from nlbone.adapters.messaging import InMemoryEventBus
|
|
18
|
+
from nlbone.config.settings import Settings
|
|
18
19
|
from nlbone.core.ports import EventBusPort
|
|
19
|
-
from nlbone.core.ports.cache import
|
|
20
|
+
from nlbone.core.ports.cache import AsyncCachePort, CachePort
|
|
20
21
|
from nlbone.core.ports.files import AsyncFileServicePort, FileServicePort
|
|
21
22
|
|
|
22
23
|
|
|
@@ -36,11 +37,15 @@ class Container(containers.DeclarativeContainer):
|
|
|
36
37
|
# --- Services ---
|
|
37
38
|
auth: providers.Singleton[KeycloakAuthService] = providers.Singleton(KeycloakAuthService, settings=config)
|
|
38
39
|
token_provider = providers.Singleton(ClientTokenProvider, auth=auth, skew_seconds=30)
|
|
39
|
-
file_service: providers.Singleton[FileServicePort] = providers.Singleton(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
file_service: providers.Singleton[FileServicePort] = providers.Singleton(
|
|
41
|
+
UploadchiClient, token_provider=token_provider
|
|
42
|
+
)
|
|
43
|
+
afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(
|
|
44
|
+
UploadchiAsyncClient, token_provider=token_provider
|
|
45
|
+
)
|
|
46
|
+
pricing_service: providers.Singleton[PricingService] = providers.Singleton(
|
|
47
|
+
PricingService, token_provider=token_provider
|
|
48
|
+
)
|
|
44
49
|
|
|
45
50
|
cache: providers.Singleton[CachePort] = providers.Selector(
|
|
46
51
|
config.CACHE_BACKEND,
|
|
@@ -58,12 +63,12 @@ class Container(containers.DeclarativeContainer):
|
|
|
58
63
|
def create_container(settings: Optional[Any] = None) -> Container:
|
|
59
64
|
c = Container()
|
|
60
65
|
if settings is not None:
|
|
61
|
-
if
|
|
66
|
+
if isinstance(settings, Settings):
|
|
67
|
+
c.config.override(settings)
|
|
68
|
+
elif hasattr(settings, "model_dump"):
|
|
62
69
|
c.config.from_dict(settings.model_dump()) # Pydantic v2
|
|
63
70
|
elif hasattr(settings, "dict"):
|
|
64
71
|
c.config.from_dict(settings.dict()) # Pydantic v1
|
|
65
72
|
elif isinstance(settings, Mapping):
|
|
66
73
|
c.config.from_dict(dict(settings))
|
|
67
|
-
else:
|
|
68
|
-
c.config.override(settings)
|
|
69
74
|
return c
|
nlbone/core/domain/models.py
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import uuid
|
|
2
2
|
from datetime import datetime
|
|
3
|
-
|
|
3
|
+
|
|
4
4
|
from sqlalchemy import JSON as SA_JSON
|
|
5
|
+
from sqlalchemy import DateTime, Index, String, Text
|
|
5
6
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
6
7
|
from sqlalchemy.sql import func
|
|
7
8
|
|
|
@@ -9,6 +10,7 @@ from nlbone.adapters.db import Base
|
|
|
9
10
|
|
|
10
11
|
try:
|
|
11
12
|
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
|
13
|
+
|
|
12
14
|
JSONType = JSONB
|
|
13
15
|
UUIDType = UUID(as_uuid=True)
|
|
14
16
|
except Exception:
|
|
@@ -35,4 +37,4 @@ class AuditLog(Base):
|
|
|
35
37
|
__table_args__ = (
|
|
36
38
|
Index("ix_audit_entity_entityid", "entity", "entity_id"),
|
|
37
39
|
Index("ix_audit_created_at", "created_at"),
|
|
38
|
-
)
|
|
40
|
+
)
|
nlbone/core/ports/cache.py
CHANGED
|
@@ -1,37 +1,53 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Any, Callable, Iterable, Mapping, Optional, Protocol, Sequence, TypeVar
|
|
2
2
|
|
|
3
3
|
T = TypeVar("T")
|
|
4
4
|
|
|
5
|
+
|
|
5
6
|
class CachePort(Protocol):
|
|
6
7
|
def get(self, key: str) -> Optional[bytes]: ...
|
|
7
|
-
def set(
|
|
8
|
+
def set(
|
|
9
|
+
self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
10
|
+
) -> None: ...
|
|
8
11
|
def delete(self, key: str) -> None: ...
|
|
9
12
|
def exists(self, key: str) -> bool: ...
|
|
10
13
|
def ttl(self, key: str) -> Optional[int]: ...
|
|
11
14
|
|
|
12
15
|
def mget(self, keys: Sequence[str]) -> list[Optional[bytes]]: ...
|
|
13
|
-
def mset(
|
|
16
|
+
def mset(
|
|
17
|
+
self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
18
|
+
) -> None: ...
|
|
14
19
|
|
|
15
20
|
def get_json(self, key: str) -> Optional[Any]: ...
|
|
16
|
-
def set_json(
|
|
21
|
+
def set_json(
|
|
22
|
+
self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
23
|
+
) -> None: ...
|
|
17
24
|
|
|
18
25
|
def invalidate_tags(self, tags: Iterable[str]) -> int: ...
|
|
19
26
|
def bump_namespace(self, namespace: str) -> int: ... # versioned keys
|
|
20
27
|
def clear_namespace(self, namespace: str) -> int: ...
|
|
21
28
|
|
|
22
|
-
def get_or_set(
|
|
29
|
+
def get_or_set(
|
|
30
|
+
self, key: str, producer: Callable[[], bytes], *, ttl: int, tags: Optional[Iterable[str]] = None
|
|
31
|
+
) -> bytes: ...
|
|
32
|
+
|
|
23
33
|
|
|
24
34
|
class AsyncCachePort(Protocol):
|
|
25
35
|
async def get(self, key: str) -> Optional[bytes]: ...
|
|
26
|
-
async def set(
|
|
36
|
+
async def set(
|
|
37
|
+
self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
38
|
+
) -> None: ...
|
|
27
39
|
async def delete(self, key: str) -> None: ...
|
|
28
40
|
async def exists(self, key: str) -> bool: ...
|
|
29
41
|
async def ttl(self, key: str) -> Optional[int]: ...
|
|
30
42
|
async def mget(self, keys: Sequence[str]) -> list[Optional[bytes]]: ...
|
|
31
|
-
async def mset(
|
|
43
|
+
async def mset(
|
|
44
|
+
self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
45
|
+
) -> None: ...
|
|
32
46
|
async def get_json(self, key: str) -> Optional[Any]: ...
|
|
33
|
-
async def set_json(
|
|
47
|
+
async def set_json(
|
|
48
|
+
self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
49
|
+
) -> None: ...
|
|
34
50
|
async def invalidate_tags(self, tags: Iterable[str]) -> int: ...
|
|
35
51
|
async def bump_namespace(self, namespace: str) -> int: ...
|
|
36
52
|
async def clear_namespace(self, namespace: str) -> int: ...
|
|
37
|
-
async def get_or_set(self, key: str, producer, *, ttl: int, tags: Optional[Iterable[str]] = None) -> bytes: ...
|
|
53
|
+
async def get_or_set(self, key: str, producer, *, ttl: int, tags: Optional[Iterable[str]] = None) -> bytes: ...
|
|
@@ -59,3 +59,29 @@ def has_access(*, permissions=None):
|
|
|
59
59
|
return wrapper
|
|
60
60
|
|
|
61
61
|
return decorator
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def client_or_user_has_access(*, permissions=None, client_permissions=None):
|
|
65
|
+
def decorator(func):
|
|
66
|
+
@functools.wraps(func)
|
|
67
|
+
def wrapper(*args, **kwargs):
|
|
68
|
+
request = current_request()
|
|
69
|
+
token = getattr(request.state, "token", None)
|
|
70
|
+
if not token:
|
|
71
|
+
raise UnauthorizedException()
|
|
72
|
+
|
|
73
|
+
auth = KeycloakAuthService()
|
|
74
|
+
|
|
75
|
+
if auth.get_client_id(token):
|
|
76
|
+
needed = client_permissions or permissions
|
|
77
|
+
if not auth.client_has_access(token, permissions=needed):
|
|
78
|
+
raise ForbiddenException(f"Forbidden (client) {needed}")
|
|
79
|
+
else:
|
|
80
|
+
if not current_user_id():
|
|
81
|
+
raise UnauthorizedException()
|
|
82
|
+
if not auth.has_access(token, permissions=permissions):
|
|
83
|
+
raise ForbiddenException(f"Forbidden (user) {permissions}")
|
|
84
|
+
|
|
85
|
+
return func(*args, **kwargs)
|
|
86
|
+
return wrapper
|
|
87
|
+
return decorator
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from math import ceil
|
|
3
|
-
from typing import Any, Optional
|
|
3
|
+
from typing import Any, Optional, List
|
|
4
4
|
|
|
5
5
|
from fastapi import Query
|
|
6
6
|
|
|
@@ -13,16 +13,18 @@ class PaginateRequest:
|
|
|
13
13
|
"""
|
|
14
14
|
|
|
15
15
|
def __init__(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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,
|
|
21
22
|
) -> None:
|
|
22
23
|
self.limit = max(0, limit)
|
|
23
24
|
self.offset = max(0, offset)
|
|
24
25
|
self.sort = self._parse_sort(sort)
|
|
25
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 [])[:50]
|
|
26
28
|
|
|
27
29
|
@staticmethod
|
|
28
30
|
def _parse_sort(sort_str: Optional[str]) -> list[dict[str, str]]:
|
|
@@ -81,12 +83,12 @@ class PaginateResponse:
|
|
|
81
83
|
"""
|
|
82
84
|
|
|
83
85
|
def __init__(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
self,
|
|
87
|
+
data: list[Any],
|
|
88
|
+
total_count: int | None,
|
|
89
|
+
limit: int,
|
|
90
|
+
offset: int,
|
|
91
|
+
use_data_key: bool = True,
|
|
90
92
|
) -> None:
|
|
91
93
|
self.data = data
|
|
92
94
|
self.total_count = total_count
|
nlbone/interfaces/cli/init_db.py
CHANGED
nlbone/interfaces/cli/main.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import typer
|
|
2
1
|
from typing import Optional
|
|
3
2
|
|
|
3
|
+
import typer
|
|
4
|
+
|
|
4
5
|
from nlbone.adapters.db import init_sync_engine
|
|
5
6
|
from nlbone.config.settings import get_settings
|
|
6
7
|
from nlbone.interfaces.cli.init_db import init_db_command
|
|
@@ -9,12 +10,10 @@ app = typer.Typer(help="NLBone CLI")
|
|
|
9
10
|
|
|
10
11
|
app.add_typer(init_db_command, name="db")
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
@app.callback()
|
|
13
15
|
def common(
|
|
14
|
-
env_file: Optional[str] = typer.Option(
|
|
15
|
-
None, "--env-file", "-e",
|
|
16
|
-
help="Path to .env file. In prod omit this."
|
|
17
|
-
),
|
|
16
|
+
env_file: Optional[str] = typer.Option(None, "--env-file", "-e", help="Path to .env file. In prod omit this."),
|
|
18
17
|
debug: bool = typer.Option(False, "--debug", help="Enable debug logging"),
|
|
19
18
|
):
|
|
20
19
|
settings = get_settings(env_file=env_file)
|
|
@@ -22,8 +21,10 @@ def common(
|
|
|
22
21
|
pass
|
|
23
22
|
init_sync_engine(echo=settings.DEBUG)
|
|
24
23
|
|
|
24
|
+
|
|
25
25
|
def main():
|
|
26
26
|
app()
|
|
27
27
|
|
|
28
|
+
|
|
28
29
|
if __name__ == "__main__":
|
|
29
30
|
main()
|