nlbone 0.6.0__py3-none-any.whl → 0.6.8__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/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 +22 -18
- 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 +10 -6
- 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/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.8.dist-info}/METADATA +1 -1
- {nlbone-0.6.0.dist-info → nlbone-0.6.8.dist-info}/RECORD +37 -37
- {nlbone-0.6.0.dist-info → nlbone-0.6.8.dist-info}/WHEEL +0 -0
- {nlbone-0.6.0.dist-info → nlbone-0.6.8.dist-info}/entry_points.txt +0 -0
- {nlbone-0.6.0.dist-info → nlbone-0.6.8.dist-info}/licenses/LICENSE +0 -0
nlbone/adapters/__init__.py
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import nlbone.adapters.db.postgres.audit
|
nlbone/adapters/auth/keycloak.py
CHANGED
|
@@ -75,4 +75,4 @@ class KeycloakAuthService(AuthService):
|
|
|
75
75
|
def client_has_access(self, token: str, permissions: list[str], allowed_clients: set[str] | None = None) -> bool:
|
|
76
76
|
if not self.is_client_token(token, allowed_clients):
|
|
77
77
|
return False
|
|
78
|
-
return self.has_access(token, permissions)
|
|
78
|
+
return self.has_access(token, permissions)
|
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
import os
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Any, Iterable, Mapping, Optional, Sequence
|
|
5
5
|
|
|
6
6
|
from redis.asyncio import Redis
|
|
7
|
+
|
|
7
8
|
from nlbone.core.ports.cache import AsyncCachePort
|
|
8
9
|
|
|
9
10
|
|
|
10
|
-
def _nsver_key(ns: str) -> str:
|
|
11
|
-
|
|
11
|
+
def _nsver_key(ns: str) -> str:
|
|
12
|
+
return f"nsver:{ns}"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _tag_key(tag: str) -> str:
|
|
16
|
+
return f"tag:{tag}"
|
|
17
|
+
|
|
12
18
|
|
|
13
19
|
class AsyncRedisCache(AsyncCachePort):
|
|
14
20
|
def __init__(self, url: str, *, invalidate_channel: str | None = None):
|
|
@@ -36,7 +42,9 @@ class AsyncRedisCache(AsyncCachePort):
|
|
|
36
42
|
fk = await self._full_key(key)
|
|
37
43
|
return await self._r.get(fk)
|
|
38
44
|
|
|
39
|
-
async def set(
|
|
45
|
+
async def set(
|
|
46
|
+
self, key: str, value: bytes, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
47
|
+
) -> None:
|
|
40
48
|
fk = await self._full_key(key)
|
|
41
49
|
if ttl is None:
|
|
42
50
|
await self._r.set(fk, value)
|
|
@@ -66,8 +74,9 @@ class AsyncRedisCache(AsyncCachePort):
|
|
|
66
74
|
fks = [await self._full_key(k) for k in keys]
|
|
67
75
|
return await self._r.mget(fks)
|
|
68
76
|
|
|
69
|
-
async def mset(
|
|
70
|
-
|
|
77
|
+
async def mset(
|
|
78
|
+
self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
79
|
+
) -> None:
|
|
71
80
|
pipe = self._r.pipeline()
|
|
72
81
|
if ttl is None:
|
|
73
82
|
for k, v in items.items():
|
|
@@ -93,8 +102,9 @@ class AsyncRedisCache(AsyncCachePort):
|
|
|
93
102
|
b = await self.get(key)
|
|
94
103
|
return None if b is None else json.loads(b)
|
|
95
104
|
|
|
96
|
-
async def set_json(
|
|
97
|
-
|
|
105
|
+
async def set_json(
|
|
106
|
+
self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
107
|
+
) -> None:
|
|
98
108
|
await self.set(key, json.dumps(value).encode("utf-8"), ttl=ttl, tags=tags)
|
|
99
109
|
|
|
100
110
|
# -------- invalidation --------
|
nlbone/adapters/cache/memory.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import json
|
|
2
|
-
|
|
1
|
+
import json
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from typing import Any, Dict, Iterable, Mapping, Optional, Sequence, Set
|
|
5
|
+
|
|
3
6
|
from nlbone.core.ports.cache import CachePort
|
|
4
7
|
|
|
5
8
|
|
|
@@ -12,7 +15,8 @@ class InMemoryCache(CachePort):
|
|
|
12
15
|
|
|
13
16
|
def _expired(self, key: str) -> bool:
|
|
14
17
|
v = self._data.get(key)
|
|
15
|
-
if not v:
|
|
18
|
+
if not v:
|
|
19
|
+
return True
|
|
16
20
|
_, exp = v
|
|
17
21
|
return exp is not None and time.time() > exp
|
|
18
22
|
|
|
@@ -21,7 +25,8 @@ class InMemoryCache(CachePort):
|
|
|
21
25
|
self._data.pop(key, None)
|
|
22
26
|
|
|
23
27
|
def _attach_tags(self, key: str, tags: Optional[Iterable[str]]) -> None:
|
|
24
|
-
if not tags:
|
|
28
|
+
if not tags:
|
|
29
|
+
return
|
|
25
30
|
for t in tags:
|
|
26
31
|
self._tags.setdefault(t, set()).add(key)
|
|
27
32
|
|
|
@@ -50,17 +55,20 @@ class InMemoryCache(CachePort):
|
|
|
50
55
|
with self._lock:
|
|
51
56
|
self._gc(key)
|
|
52
57
|
v = self._data.get(key)
|
|
53
|
-
if not v:
|
|
58
|
+
if not v:
|
|
59
|
+
return None
|
|
54
60
|
_, exp = v
|
|
55
|
-
if exp is None:
|
|
61
|
+
if exp is None:
|
|
62
|
+
return None
|
|
56
63
|
rem = int(exp - time.time())
|
|
57
64
|
return rem if rem >= 0 else 0
|
|
58
65
|
|
|
59
66
|
def mget(self, keys: Sequence[str]) -> list[Optional[bytes]]:
|
|
60
67
|
return [self.get(k) for k in keys]
|
|
61
68
|
|
|
62
|
-
def mset(
|
|
63
|
-
|
|
69
|
+
def mset(
|
|
70
|
+
self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
71
|
+
) -> None:
|
|
64
72
|
for k, v in items.items():
|
|
65
73
|
self.set(k, v, ttl=ttl, tags=tags)
|
|
66
74
|
|
|
@@ -68,8 +76,9 @@ class InMemoryCache(CachePort):
|
|
|
68
76
|
b = self.get(key)
|
|
69
77
|
return None if b is None else json.loads(b)
|
|
70
78
|
|
|
71
|
-
def set_json(
|
|
72
|
-
|
|
79
|
+
def set_json(
|
|
80
|
+
self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
81
|
+
) -> None:
|
|
73
82
|
self.set(key, json.dumps(value).encode("utf-8"), ttl=ttl, tags=tags)
|
|
74
83
|
|
|
75
84
|
def invalidate_tags(self, tags: Iterable[str]) -> int:
|
|
@@ -91,7 +100,8 @@ class InMemoryCache(CachePort):
|
|
|
91
100
|
def clear_namespace(self, namespace: str) -> int:
|
|
92
101
|
with self._lock:
|
|
93
102
|
keys = [k for k in self._data.keys() if k.startswith(namespace + ":")]
|
|
94
|
-
for k in keys:
|
|
103
|
+
for k in keys:
|
|
104
|
+
self.delete(k)
|
|
95
105
|
return len(keys)
|
|
96
106
|
|
|
97
107
|
def get_or_set(self, key: str, producer, *, ttl: int, tags=None) -> bytes:
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
import asyncio
|
|
3
4
|
import json
|
|
4
5
|
from typing import Awaitable, Callable, Optional
|
|
6
|
+
|
|
5
7
|
from redis.asyncio import Redis
|
|
6
8
|
|
|
9
|
+
|
|
7
10
|
async def run_cache_invalidation_listener(
|
|
8
11
|
redis: Redis,
|
|
9
12
|
channel: str = "cache:invalidate",
|
nlbone/adapters/cache/redis.py
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Iterable, Mapping, Optional, Sequence
|
|
7
|
+
|
|
4
8
|
import redis # redis-py (sync)
|
|
9
|
+
|
|
5
10
|
from nlbone.core.ports.cache import CachePort
|
|
6
11
|
|
|
7
12
|
|
|
13
|
+
def _nsver_key(ns: str) -> str:
|
|
14
|
+
return f"nsver:{ns}"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _tag_key(tag: str) -> str:
|
|
18
|
+
return f"tag:{tag}"
|
|
8
19
|
|
|
9
|
-
def _nsver_key(ns: str) -> str: return f"nsver:{ns}"
|
|
10
|
-
def _tag_key(tag: str) -> str: return f"tag:{tag}"
|
|
11
20
|
|
|
12
21
|
class RedisCache(CachePort):
|
|
13
22
|
def __init__(self, url: str):
|
|
@@ -57,7 +66,9 @@ class RedisCache(CachePort):
|
|
|
57
66
|
fks = [self._full_key(k) for k in keys]
|
|
58
67
|
return self.r.mget(fks)
|
|
59
68
|
|
|
60
|
-
def mset(
|
|
69
|
+
def mset(
|
|
70
|
+
self, items: Mapping[str, bytes], *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
71
|
+
) -> None:
|
|
61
72
|
pipe = self.r.pipeline()
|
|
62
73
|
if ttl is None:
|
|
63
74
|
for k, v in items.items():
|
|
@@ -77,7 +88,9 @@ class RedisCache(CachePort):
|
|
|
77
88
|
b = self.get(key)
|
|
78
89
|
return None if b is None else json.loads(b)
|
|
79
90
|
|
|
80
|
-
def set_json(
|
|
91
|
+
def set_json(
|
|
92
|
+
self, key: str, value: Any, *, ttl: Optional[int] = None, tags: Optional[Iterable[str]] = None
|
|
93
|
+
) -> None:
|
|
81
94
|
self.set(key, json.dumps(value).encode("utf-8"), ttl=ttl, tags=tags)
|
|
82
95
|
|
|
83
96
|
def invalidate_tags(self, tags: Iterable[str]) -> int:
|
|
@@ -109,8 +122,10 @@ class RedisCache(CachePort):
|
|
|
109
122
|
while True:
|
|
110
123
|
cursor, keys = self.r.scan(cursor=cursor, match=pattern, count=1000)
|
|
111
124
|
if keys:
|
|
112
|
-
self.r.delete(*keys)
|
|
113
|
-
|
|
125
|
+
self.r.delete(*keys)
|
|
126
|
+
cnt += len(keys)
|
|
127
|
+
if cursor == 0:
|
|
128
|
+
break
|
|
114
129
|
return cnt
|
|
115
130
|
|
|
116
131
|
def get_or_set(self, key: str, producer, *, ttl: int, tags=None) -> bytes:
|
nlbone/adapters/db/__init__.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import uuid
|
|
2
2
|
from datetime import date, datetime
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from enum import Enum as _Enum
|
|
3
5
|
from typing import Any
|
|
4
|
-
|
|
6
|
+
|
|
7
|
+
from sqlalchemy import event
|
|
8
|
+
from sqlalchemy import inspect as sa_inspect
|
|
5
9
|
from sqlalchemy.orm import Session as SASession
|
|
6
|
-
from enum import Enum as _Enum
|
|
7
|
-
from decimal import Decimal
|
|
8
10
|
|
|
9
11
|
from nlbone.core.domain.models import AuditLog
|
|
10
12
|
from nlbone.utils.context import current_context_dict
|
|
@@ -54,8 +56,11 @@ def _ser(val):
|
|
|
54
56
|
|
|
55
57
|
|
|
56
58
|
def _entity_name(obj: Any) -> str:
|
|
57
|
-
return
|
|
58
|
-
|
|
59
|
+
return (
|
|
60
|
+
getattr(getattr(obj, "__table__", None), "name", None)
|
|
61
|
+
or getattr(obj, "__tablename__", None)
|
|
62
|
+
or obj.__class__.__name__
|
|
63
|
+
)
|
|
59
64
|
|
|
60
65
|
|
|
61
66
|
def _entity_id(obj: Any) -> str:
|
|
@@ -84,13 +89,15 @@ def _changes_for_update(obj: any) -> dict[str, dict[str, any]]:
|
|
|
84
89
|
except KeyError:
|
|
85
90
|
continue
|
|
86
91
|
|
|
87
|
-
hist = state.history
|
|
92
|
+
hist = state.history # History object
|
|
88
93
|
if hist.has_changes():
|
|
89
94
|
old = hist.deleted[0] if hist.deleted else None
|
|
90
95
|
new = hist.added[0] if hist.added else None
|
|
91
96
|
if old != new:
|
|
92
97
|
changes[key] = {"old": _ser(old), "new": _ser(new)}
|
|
93
98
|
return changes
|
|
99
|
+
|
|
100
|
+
|
|
94
101
|
@event.listens_for(SASession, "before_flush")
|
|
95
102
|
def before_flush(session: SASession, flush_context, instances):
|
|
96
103
|
entries = session.info.setdefault("_audit_entries", [])
|
|
@@ -107,11 +114,7 @@ def before_flush(session: SASession, flush_context, instances):
|
|
|
107
114
|
if key in exclude:
|
|
108
115
|
continue
|
|
109
116
|
row[key] = _ser(getattr(obj, key, None))
|
|
110
|
-
entries.append({
|
|
111
|
-
"obj": obj,
|
|
112
|
-
"op": "INSERT",
|
|
113
|
-
"changes": {k: {"old": None, "new": v} for k, v in row.items()}
|
|
114
|
-
})
|
|
117
|
+
entries.append({"obj": obj, "op": "INSERT", "changes": {k: {"old": None, "new": v} for k, v in row.items()}})
|
|
115
118
|
|
|
116
119
|
# UPDATE
|
|
117
120
|
for obj in session.dirty:
|
|
@@ -9,10 +9,7 @@ class RedisClient:
|
|
|
9
9
|
@classmethod
|
|
10
10
|
def get_client(cls) -> redis.Redis:
|
|
11
11
|
if cls._client is None:
|
|
12
|
-
cls._client = redis.from_url(
|
|
13
|
-
get_settings().REDIS_URL,
|
|
14
|
-
decode_responses=True
|
|
15
|
-
)
|
|
12
|
+
cls._client = redis.from_url(get_settings().REDIS_URL, decode_responses=True)
|
|
16
13
|
return cls._client
|
|
17
14
|
|
|
18
15
|
@classmethod
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
from .
|
|
2
|
-
from .
|
|
1
|
+
from .pricing import CalculatePriceIn, CalculatePriceOut, PricingService
|
|
2
|
+
from .uploadchi import UploadchiAsyncClient, UploadchiClient, UploadchiError
|
|
@@ -1 +1 @@
|
|
|
1
|
-
from .pricing_service import
|
|
1
|
+
from .pricing_service import CalculatePriceIn, CalculatePriceOut, PricingService
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import List, Literal, Optional
|
|
3
3
|
|
|
4
4
|
import httpx
|
|
5
5
|
import requests
|
|
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field, NonNegativeInt, RootModel
|
|
|
7
7
|
|
|
8
8
|
from nlbone.adapters.auth.token_provider import ClientTokenProvider
|
|
9
9
|
from nlbone.config.settings import get_settings
|
|
10
|
-
from nlbone.utils.http import
|
|
10
|
+
from nlbone.utils.http import auth_headers, normalize_https_base
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class PricingError(Exception):
|
|
@@ -33,10 +33,17 @@ class Product(BaseModel):
|
|
|
33
33
|
|
|
34
34
|
class Pricing(BaseModel):
|
|
35
35
|
source: Literal["formula", "static"]
|
|
36
|
-
price: float
|
|
36
|
+
price: Optional[float] = None
|
|
37
37
|
discount: Optional[float] = None
|
|
38
38
|
discount_type: Optional[DiscountType] = None
|
|
39
|
-
params: dict
|
|
39
|
+
params: Optional[dict] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Segment(BaseModel):
|
|
43
|
+
id: str
|
|
44
|
+
name: str
|
|
45
|
+
specificity: int
|
|
46
|
+
matched_fields: list
|
|
40
47
|
|
|
41
48
|
|
|
42
49
|
class Formula(BaseModel):
|
|
@@ -49,10 +56,8 @@ class Formula(BaseModel):
|
|
|
49
56
|
|
|
50
57
|
class PricingRule(BaseModel):
|
|
51
58
|
product: Product
|
|
52
|
-
|
|
59
|
+
segment: Segment | None
|
|
53
60
|
formula: Optional[Formula] = None
|
|
54
|
-
specificity: int
|
|
55
|
-
matched_fields: list
|
|
56
61
|
pricing: Pricing
|
|
57
62
|
|
|
58
63
|
|
|
@@ -62,11 +67,11 @@ class CalculatePriceOut(RootModel[List[PricingRule]]):
|
|
|
62
67
|
|
|
63
68
|
class PricingService:
|
|
64
69
|
def __init__(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
self,
|
|
71
|
+
token_provider: ClientTokenProvider,
|
|
72
|
+
base_url: Optional[str] = None,
|
|
73
|
+
timeout_seconds: Optional[float] = None,
|
|
74
|
+
client: httpx.Client | None = None,
|
|
70
75
|
) -> None:
|
|
71
76
|
s = get_settings()
|
|
72
77
|
self._base_url = normalize_https_base(base_url or str(s.PRICING_SERVICE_URL), enforce_https=False)
|
|
@@ -74,17 +79,16 @@ class PricingService:
|
|
|
74
79
|
self._client = client or requests.session()
|
|
75
80
|
self._token_provider = token_provider
|
|
76
81
|
|
|
77
|
-
def calculate(self, items: list[CalculatePriceIn]) -> CalculatePriceOut:
|
|
78
|
-
body = {
|
|
79
|
-
"items": [i.model_dump() for i in items]
|
|
80
|
-
}
|
|
82
|
+
def calculate(self, items: list[CalculatePriceIn], response: Literal["list", "dict"] = "dict") -> CalculatePriceOut:
|
|
83
|
+
body = {"items": [i.model_dump() for i in items]}
|
|
81
84
|
|
|
82
85
|
r = self._client.post(
|
|
83
|
-
f"{self._base_url}/
|
|
86
|
+
f"{self._base_url}/price/calculate",
|
|
87
|
+
params={"response": response},
|
|
84
88
|
headers=auth_headers(self._token_provider.get_access_token()),
|
|
85
89
|
json=body,
|
|
86
90
|
timeout=self._timeout,
|
|
87
|
-
verify=False
|
|
91
|
+
verify=False,
|
|
88
92
|
)
|
|
89
93
|
|
|
90
94
|
if r.status_code not in (200, 204):
|
|
@@ -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
|
@@ -16,7 +16,7 @@ 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
18
|
from nlbone.core.ports import EventBusPort
|
|
19
|
-
from nlbone.core.ports.cache import
|
|
19
|
+
from nlbone.core.ports.cache import AsyncCachePort, CachePort
|
|
20
20
|
from nlbone.core.ports.files import AsyncFileServicePort, FileServicePort
|
|
21
21
|
|
|
22
22
|
|
|
@@ -36,11 +36,15 @@ class Container(containers.DeclarativeContainer):
|
|
|
36
36
|
# --- Services ---
|
|
37
37
|
auth: providers.Singleton[KeycloakAuthService] = providers.Singleton(KeycloakAuthService, settings=config)
|
|
38
38
|
token_provider = providers.Singleton(ClientTokenProvider, auth=auth, skew_seconds=30)
|
|
39
|
-
file_service: providers.Singleton[FileServicePort] = providers.Singleton(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
file_service: providers.Singleton[FileServicePort] = providers.Singleton(
|
|
40
|
+
UploadchiClient, token_provider=token_provider
|
|
41
|
+
)
|
|
42
|
+
afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(
|
|
43
|
+
UploadchiAsyncClient, token_provider=token_provider
|
|
44
|
+
)
|
|
45
|
+
pricing_service: providers.Singleton[PricingService] = providers.Singleton(
|
|
46
|
+
PricingService, token_provider=token_provider
|
|
47
|
+
)
|
|
44
48
|
|
|
45
49
|
cache: providers.Singleton[CachePort] = providers.Selector(
|
|
46
50
|
config.CACHE_BACKEND,
|
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
|
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()
|
nlbone/utils/cache.py
CHANGED
|
@@ -4,17 +4,20 @@ import json
|
|
|
4
4
|
from typing import Any, Callable, Iterable, Optional
|
|
5
5
|
|
|
6
6
|
from makefun import wraps as mf_wraps
|
|
7
|
+
|
|
7
8
|
from nlbone.utils.cache_registry import get_cache
|
|
8
9
|
|
|
9
10
|
try:
|
|
10
11
|
from pydantic import BaseModel # v1/v2
|
|
11
12
|
except Exception: # pragma: no cover
|
|
13
|
+
|
|
12
14
|
class BaseModel: # minimal fallback
|
|
13
15
|
pass
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
# -------- helpers --------
|
|
17
19
|
|
|
20
|
+
|
|
18
21
|
def _bind(func: Callable, args, kwargs):
|
|
19
22
|
sig = inspect.signature(func)
|
|
20
23
|
bound = sig.bind_partial(*args, **kwargs)
|
|
@@ -79,6 +82,7 @@ def _run_maybe_async(func: Callable, *args, **kwargs):
|
|
|
79
82
|
|
|
80
83
|
# -------- cache decorators --------
|
|
81
84
|
|
|
85
|
+
|
|
82
86
|
def cached(
|
|
83
87
|
*,
|
|
84
88
|
ttl: int,
|
|
@@ -94,10 +98,12 @@ def cached(
|
|
|
94
98
|
- Works with sync/async cache backends (CachePort / AsyncCachePort).
|
|
95
99
|
- `key` & `tags` are string templates, e.g. "file:{file_id}".
|
|
96
100
|
"""
|
|
101
|
+
|
|
97
102
|
def deco(func: Callable):
|
|
98
103
|
is_async_func = asyncio.iscoroutinefunction(func)
|
|
99
104
|
|
|
100
105
|
if is_async_func:
|
|
106
|
+
|
|
101
107
|
@mf_wraps(func)
|
|
102
108
|
async def aw(*args, **kwargs):
|
|
103
109
|
cache = (cache_resolver or get_cache)()
|
|
@@ -165,10 +171,12 @@ def invalidate_by_tags(tags_builder: Callable[..., Iterable[str]]):
|
|
|
165
171
|
Invalidate computed tags after function finishes.
|
|
166
172
|
Works with sync or async functions and cache backends.
|
|
167
173
|
"""
|
|
174
|
+
|
|
168
175
|
def deco(func: Callable):
|
|
169
176
|
is_async_func = asyncio.iscoroutinefunction(func)
|
|
170
177
|
|
|
171
178
|
if is_async_func:
|
|
179
|
+
|
|
172
180
|
@mf_wraps(func)
|
|
173
181
|
async def aw(*args, **kwargs):
|
|
174
182
|
out = await func(*args, **kwargs)
|
|
@@ -179,6 +187,7 @@ def invalidate_by_tags(tags_builder: Callable[..., Iterable[str]]):
|
|
|
179
187
|
else:
|
|
180
188
|
cache.invalidate_tags(tags)
|
|
181
189
|
return out
|
|
190
|
+
|
|
182
191
|
return aw
|
|
183
192
|
|
|
184
193
|
@mf_wraps(func)
|
|
@@ -191,6 +200,7 @@ def invalidate_by_tags(tags_builder: Callable[..., Iterable[str]]):
|
|
|
191
200
|
else:
|
|
192
201
|
cache.invalidate_tags(tags)
|
|
193
202
|
return out
|
|
203
|
+
|
|
194
204
|
return sw
|
|
195
205
|
|
|
196
206
|
return deco
|
nlbone/utils/cache_keys.py
CHANGED
|
@@ -3,21 +3,26 @@ import json
|
|
|
3
3
|
import random
|
|
4
4
|
from typing import Any, Mapping
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
def _stable_params(params: Mapping[str, Any]) -> str:
|
|
7
8
|
return json.dumps(params, sort_keys=True, separators=(",", ":"))
|
|
8
9
|
|
|
10
|
+
|
|
9
11
|
def make_key(ns: str, *parts: str) -> str:
|
|
10
12
|
safe_parts = [p.replace(" ", "_") for p in parts if p]
|
|
11
13
|
return f"{ns}:{':'.join(safe_parts)}" if safe_parts else f"{ns}:root"
|
|
12
14
|
|
|
15
|
+
|
|
13
16
|
def make_param_key(ns: str, base: str, params: Mapping[str, Any]) -> str:
|
|
14
17
|
payload = _stable_params(params)
|
|
15
18
|
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
|
|
16
19
|
return f"{ns}:{base}:{digest}"
|
|
17
20
|
|
|
21
|
+
|
|
18
22
|
def tag_entity(ns: str, entity_id: Any) -> str:
|
|
19
23
|
return f"{ns}:{entity_id}"
|
|
20
24
|
|
|
25
|
+
|
|
21
26
|
def tag_list(ns: str, **filters) -> str:
|
|
22
27
|
if not filters:
|
|
23
28
|
return f"{ns}:list"
|
|
@@ -25,6 +30,7 @@ def tag_list(ns: str, **filters) -> str:
|
|
|
25
30
|
digest = hashlib.md5(payload.encode("utf-8")).hexdigest()[:12]
|
|
26
31
|
return f"{ns}:list:{digest}"
|
|
27
32
|
|
|
33
|
+
|
|
28
34
|
def ttl_with_jitter(base_ttl: int, *, jitter_ratio: float = 0.1) -> int:
|
|
29
35
|
jitter = int(base_ttl * jitter_ratio)
|
|
30
36
|
return base_ttl + random.randint(-jitter, jitter)
|
nlbone/utils/cache_registry.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from typing import Callable, Optional, TypeVar
|
|
2
1
|
from contextvars import ContextVar
|
|
2
|
+
from typing import Callable, Optional, TypeVar
|
|
3
3
|
|
|
4
4
|
T = TypeVar("T")
|
|
5
5
|
|
|
@@ -7,17 +7,20 @@ _global_resolver: Optional[Callable[[], T]] = None
|
|
|
7
7
|
|
|
8
8
|
_ctx_resolver: ContextVar[Optional[Callable[[], T]]] = ContextVar("_ctx_resolver", default=None)
|
|
9
9
|
|
|
10
|
+
|
|
10
11
|
def set_cache_resolver(fn: Callable[[], T]) -> None:
|
|
11
12
|
"""Set process-wide cache resolver (e.g., lambda: container.cache())."""
|
|
12
13
|
global _global_resolver
|
|
13
14
|
_global_resolver = fn
|
|
14
15
|
|
|
16
|
+
|
|
15
17
|
def set_context_cache_resolver(fn: Optional[Callable[[], T]]) -> None:
|
|
16
18
|
"""Override resolver in current context (useful in tests/background tasks)."""
|
|
17
19
|
_ctx_resolver.set(fn)
|
|
18
20
|
|
|
21
|
+
|
|
19
22
|
def get_cache() -> T:
|
|
20
23
|
fn = _ctx_resolver.get() or _global_resolver
|
|
21
24
|
if fn is None:
|
|
22
25
|
raise RuntimeError("Cache resolver not configured. Call set_cache_resolver(...) first.")
|
|
23
|
-
return fn()
|
|
26
|
+
return fn()
|
nlbone/utils/http.py
CHANGED
|
@@ -10,7 +10,7 @@ def auth_headers(token: str | None) -> dict[str, str]:
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def build_list_query(
|
|
13
|
-
|
|
13
|
+
limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
|
|
14
14
|
) -> dict[str, Any]:
|
|
15
15
|
q: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
16
16
|
if filters:
|
nlbone/utils/redactor.py
CHANGED
|
@@ -5,6 +5,7 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
SENSITIVE_KEYS = {"password", "token", "access_token", "refresh_token", "secret", "card_number", "cvv", "pan"}
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
class PiiRedactor(logging.Filter):
|
|
9
10
|
def _redact_in_obj(self, obj: Any):
|
|
10
11
|
if isinstance(obj, dict):
|
|
@@ -29,4 +30,4 @@ class PiiRedactor(logging.Filter):
|
|
|
29
30
|
record.msg = self._redact_in_obj(record.msg)
|
|
30
31
|
except Exception:
|
|
31
32
|
pass
|
|
32
|
-
return True
|
|
33
|
+
return True
|
nlbone/utils/time.py
CHANGED
|
@@ -30,7 +30,7 @@ class TimeUtility:
|
|
|
30
30
|
|
|
31
31
|
@classmethod
|
|
32
32
|
def get_past_datetime(
|
|
33
|
-
|
|
33
|
+
cls, days=0, seconds=0, microseconds=0, milliseconds=0, minutes=0, hours=0, weeks=0
|
|
34
34
|
) -> datetime:
|
|
35
35
|
delta = timedelta(
|
|
36
36
|
days=days,
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
nlbone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
nlbone/container.py,sha256=
|
|
2
|
+
nlbone/container.py,sha256=ZD5sF1aByHUR_SoYo2Cw0_LMgGtw1pQluiG1afbzgRI,3242
|
|
3
3
|
nlbone/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
-
nlbone/adapters/__init__.py,sha256=
|
|
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=
|
|
7
|
-
nlbone/adapters/auth/token_provider.py,sha256=
|
|
6
|
+
nlbone/adapters/auth/keycloak.py,sha256=j5KWMXGZN4Sey34I-dbaHrPG37hAaDPs4Utcra6UgWY,2730
|
|
7
|
+
nlbone/adapters/auth/token_provider.py,sha256=vL2Hk6HXnBbpk40Tq1wpqak5QQ7KEQf3nRquT0N8V4Q,1433
|
|
8
8
|
nlbone/adapters/cache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
nlbone/adapters/cache/async_redis.py,sha256=
|
|
10
|
-
nlbone/adapters/cache/memory.py,sha256=
|
|
11
|
-
nlbone/adapters/cache/pubsub_listener.py,sha256=
|
|
12
|
-
nlbone/adapters/cache/redis.py,sha256=
|
|
13
|
-
nlbone/adapters/db/__init__.py,sha256=
|
|
9
|
+
nlbone/adapters/cache/async_redis.py,sha256=vvu5w4ANx0BVRHL95RAMGsD8CcaC-tSBMbCius2cuNc,6212
|
|
10
|
+
nlbone/adapters/cache/memory.py,sha256=y8M4erHQXApiSMAqG6Qk4pxEb60hRdu1szPv6iqvO9c,3738
|
|
11
|
+
nlbone/adapters/cache/pubsub_listener.py,sha256=3vfK4O4EzuQQhTsbZ_bweP06o99kDSyHJ5PrfUotUaE,1460
|
|
12
|
+
nlbone/adapters/cache/redis.py,sha256=2Y1DYHBLCrbHTO6O7pw85u3qY6OnCIFTYJ-HBvBs0FM,4608
|
|
13
|
+
nlbone/adapters/db/__init__.py,sha256=0CDSySEk4jJsqmwI0eNuaaLJOJDt8_iSiHBsFdC-L3s,212
|
|
14
14
|
nlbone/adapters/db/postgres/__init__.py,sha256=6JYJH0xZs3aR-zuyMpRhsdzFugmqz8nprwTQLprqhZc,313
|
|
15
|
-
nlbone/adapters/db/postgres/audit.py,sha256=
|
|
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
18
|
nlbone/adapters/db/postgres/query_builder.py,sha256=U5pqpCfJKuMIxIEHyodoHuPgE8jf53slC1ScKZR5xa4,8653
|
|
@@ -20,24 +20,24 @@ nlbone/adapters/db/postgres/repository.py,sha256=J_DBE73JhHPYCk90c5-O7lQtZbxDgqj
|
|
|
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
|
|
22
22
|
nlbone/adapters/db/redis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
-
nlbone/adapters/db/redis/client.py,sha256=
|
|
24
|
-
nlbone/adapters/http_clients/__init__.py,sha256=
|
|
25
|
-
nlbone/adapters/http_clients/pricing/__init__.py,sha256=
|
|
26
|
-
nlbone/adapters/http_clients/pricing/pricing_service.py,sha256=
|
|
27
|
-
nlbone/adapters/http_clients/uploadchi/__init__.py,sha256=
|
|
28
|
-
nlbone/adapters/http_clients/uploadchi/uploadchi.py,sha256=
|
|
29
|
-
nlbone/adapters/http_clients/uploadchi/uploadchi_async.py,sha256=
|
|
23
|
+
nlbone/adapters/db/redis/client.py,sha256=5SUnwP2-GrueSFimUbiqDvrQsumvIE2aeozk8l-vOfQ,466
|
|
24
|
+
nlbone/adapters/http_clients/__init__.py,sha256=w-Yr9CLuXMU71N0Ada5HbvP1DB53wqeP6B-i5rALlTo,150
|
|
25
|
+
nlbone/adapters/http_clients/pricing/__init__.py,sha256=ElA9NFcAR9u4cqb_w3PPqKU3xGeyjNLQ8veJ0ql2iz0,81
|
|
26
|
+
nlbone/adapters/http_clients/pricing/pricing_service.py,sha256=KzLRfNxrFjFi74EXahRVS7EH9qx8dbWoFB9WsmG15Og,2710
|
|
27
|
+
nlbone/adapters/http_clients/uploadchi/__init__.py,sha256=uBzEOuVtY22teWW2b36Pitkdk5yVdSqa6xbg22JfTNg,105
|
|
28
|
+
nlbone/adapters/http_clients/uploadchi/uploadchi.py,sha256=ABFiH3bLsxFtB-4Si4SEedE2bMUVz5hWXGwD4RkV3ws,4816
|
|
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
|
-
nlbone/adapters/percolation/__init__.py,sha256=
|
|
34
|
-
nlbone/adapters/percolation/connection.py,sha256=
|
|
33
|
+
nlbone/adapters/percolation/__init__.py,sha256=0h1Bw7FzxgkDIHxeoyQXSfegrhP6VbpYV4QC8njYdRE,38
|
|
34
|
+
nlbone/adapters/percolation/connection.py,sha256=yuqcboFLsd4FRfywfXatbe8NjVtzyEWHGOZ8OVmmQaI,333
|
|
35
35
|
nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
36
|
-
nlbone/config/logging.py,sha256=
|
|
36
|
+
nlbone/config/logging.py,sha256=Ot6Ctf7EQZlW8YNB-uBdleqI6wixn5fH0Eo6QRgNkQk,4358
|
|
37
37
|
nlbone/config/settings.py,sha256=W3NHZP6yjIyyKiGWNkjlUt_RYFKkcIfMBoKih_z_0Bs,3911
|
|
38
38
|
nlbone/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
39
39
|
nlbone/core/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
40
|
-
nlbone/core/application/base_worker.py,sha256=
|
|
40
|
+
nlbone/core/application/base_worker.py,sha256=5brIToSd-vi6tw0ukhHnUZGZhOLq1SQ-NRRy-kp6D24,1193
|
|
41
41
|
nlbone/core/application/events.py,sha256=eQGLE0aZHuWJsy9J-qRse4CMXOtweH9-2rQ7AIPRMEQ,614
|
|
42
42
|
nlbone/core/application/services.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
43
|
nlbone/core/application/use_case.py,sha256=3GMQZ3CFK5cbLoBNBgohPft6GBq2j9_wr8iKRq_osQA,247
|
|
@@ -45,10 +45,10 @@ nlbone/core/application/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRk
|
|
|
45
45
|
nlbone/core/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
46
|
nlbone/core/domain/base.py,sha256=5oUfbpaI8juJ28Api8J9IXOSm55VI2bp4QNhA0U8h2Y,1251
|
|
47
47
|
nlbone/core/domain/events.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
48
|
-
nlbone/core/domain/models.py,sha256=
|
|
48
|
+
nlbone/core/domain/models.py,sha256=Zn_rwtlzfjOEJZo6HS9M8UsMk-HpMJrHAKn05UA-u2k,1461
|
|
49
49
|
nlbone/core/ports/__init__.py,sha256=gx-Ubj7h-1vvnu56sNnRqmer7HHfW3rX2WLl-0AX5U0,214
|
|
50
50
|
nlbone/core/ports/auth.py,sha256=Gh0yQsxx2OD6pDH2_p-khsA-bVoypP1juuqMoSfjZUo,493
|
|
51
|
-
nlbone/core/ports/cache.py,sha256=
|
|
51
|
+
nlbone/core/ports/cache.py,sha256=8pP_z4ta7PNNG8UiSrEF4xMZRm2wLPxISZvdPt7QnxQ,2351
|
|
52
52
|
nlbone/core/ports/event_bus.py,sha256=_Om1GOOT-F325oV6_LJXtLdx4vu5i7KrpTDD3qPJXU0,325
|
|
53
53
|
nlbone/core/ports/files.py,sha256=7Ov2ITYRpPwwDTZGCeNVISg8e3A9l08jbOgpTImgfK8,1863
|
|
54
54
|
nlbone/core/ports/messaging.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -62,7 +62,7 @@ nlbone/interfaces/api/routers.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
62
62
|
nlbone/interfaces/api/schemas.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
63
|
nlbone/interfaces/api/dependencies/__init__.py,sha256=rnYRrFVZCfICQrp_PVFlzNg3BeC57yM08wn2DbOHCfk,359
|
|
64
64
|
nlbone/interfaces/api/dependencies/async_auth.py,sha256=bfxgBXhp29WqevjTG4jrdPNR-75APm4jKyHdOOtxnp4,1825
|
|
65
|
-
nlbone/interfaces/api/dependencies/auth.py,sha256=
|
|
65
|
+
nlbone/interfaces/api/dependencies/auth.py,sha256=Tp4_DjJ-7hHWiWdvPaD922DGdauuzDXl5aqALBtk-QM,2750
|
|
66
66
|
nlbone/interfaces/api/dependencies/db.py,sha256=-UD39J_86UU7ZJs2ZncpdND0yhAG0NeeeALrgSDuuFw,466
|
|
67
67
|
nlbone/interfaces/api/dependencies/uow.py,sha256=QfLEvLYLNWZJQN1k-0q0hBVtUld3D75P4j39q_RjcnE,1181
|
|
68
68
|
nlbone/interfaces/api/middleware/__init__.py,sha256=zbX2vaEAfxRMIYwO2MVY_2O6bqG5H9o7HqGpX14U3Is,158
|
|
@@ -72,20 +72,20 @@ nlbone/interfaces/api/middleware/authentication.py,sha256=ze7vCm492QsX9nPL6A-PqZ
|
|
|
72
72
|
nlbone/interfaces/api/pagination/__init__.py,sha256=sWKKQFa2Z-1SlprQOqImOa2c9exq4wueKpUL_9QM7wc,417
|
|
73
73
|
nlbone/interfaces/api/pagination/offset_base.py,sha256=B6rHxzDsxQbm-d2snM6tjgnhWyZw7zvs7fcehV0gpa0,3621
|
|
74
74
|
nlbone/interfaces/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
75
|
-
nlbone/interfaces/cli/init_db.py,sha256=
|
|
76
|
-
nlbone/interfaces/cli/main.py,sha256=
|
|
75
|
+
nlbone/interfaces/cli/init_db.py,sha256=C67n2MsJ1vzkJxC8zfUYOxFdd6mEB_vT9agxN6jWoG8,790
|
|
76
|
+
nlbone/interfaces/cli/main.py,sha256=pNldsTgplVyXa-Hx96dySO2I9gFRi49nDXv7J_dO73s,686
|
|
77
77
|
nlbone/interfaces/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
78
78
|
nlbone/interfaces/jobs/sync_tokens.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
79
|
nlbone/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
80
|
-
nlbone/utils/cache.py,sha256=
|
|
81
|
-
nlbone/utils/cache_keys.py,sha256=
|
|
82
|
-
nlbone/utils/cache_registry.py,sha256=
|
|
80
|
+
nlbone/utils/cache.py,sha256=hVfkR62o5vllDrE_nY4At10wK0It6qpZ45K1xoj10cQ,5931
|
|
81
|
+
nlbone/utils/cache_keys.py,sha256=Y2YSellHTbUOcoaNbl1jaD4r485VU_e4KXsfBWhYTBo,1075
|
|
82
|
+
nlbone/utils/cache_registry.py,sha256=w28sEfUQZAhzCCqVH5TflWQY3nyDXyEcFWt8hkuHRHw,823
|
|
83
83
|
nlbone/utils/context.py,sha256=MmclJ24BG2uvSTg1IK7J-Da9BhVFDQ5ag4Ggs2FF1_w,1600
|
|
84
|
-
nlbone/utils/http.py,sha256=
|
|
85
|
-
nlbone/utils/redactor.py,sha256
|
|
86
|
-
nlbone/utils/time.py,sha256=
|
|
87
|
-
nlbone-0.6.
|
|
88
|
-
nlbone-0.6.
|
|
89
|
-
nlbone-0.6.
|
|
90
|
-
nlbone-0.6.
|
|
91
|
-
nlbone-0.6.
|
|
84
|
+
nlbone/utils/http.py,sha256=UXUoXgQdTRNT08ho8zl-C5ekfDsD8uf-JiMQ323ooqw,872
|
|
85
|
+
nlbone/utils/redactor.py,sha256=-V4HrHmHwPi3Kez587Ek1uJlgK35qGSrwBOvcbw8Jas,1279
|
|
86
|
+
nlbone/utils/time.py,sha256=DjjyQ9GLsfXoT6NK8RDW2rOlJg3e6sF04Jw6PBUrSvg,1268
|
|
87
|
+
nlbone-0.6.8.dist-info/METADATA,sha256=GRppq6YJdTQl_op7vf2oWBZP3IX2TNznHWhTFtvXJOM,2194
|
|
88
|
+
nlbone-0.6.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
89
|
+
nlbone-0.6.8.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
|
|
90
|
+
nlbone-0.6.8.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
91
|
+
nlbone-0.6.8.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|