nlbone 0.5.0__tar.gz → 0.6.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nlbone-0.5.0 → nlbone-0.6.8}/PKG-INFO +1 -1
- {nlbone-0.5.0 → nlbone-0.6.8}/pyproject.toml +2 -2
- nlbone-0.6.8/src/nlbone/adapters/__init__.py +1 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/auth/keycloak.py +1 -1
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/auth/token_provider.py +1 -1
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/cache/async_redis.py +18 -8
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/cache/memory.py +21 -11
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/cache/pubsub_listener.py +3 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/cache/redis.py +23 -8
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/__init__.py +0 -1
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/postgres/audit.py +14 -11
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/redis/client.py +1 -4
- nlbone-0.6.8/src/nlbone/adapters/http_clients/__init__.py +2 -0
- nlbone-0.6.8/src/nlbone/adapters/http_clients/pricing/__init__.py +1 -0
- nlbone-0.6.8/src/nlbone/adapters/http_clients/pricing/pricing_service.py +100 -0
- nlbone-0.6.8/src/nlbone/adapters/http_clients/uploadchi/__init__.py +2 -0
- {nlbone-0.5.0/src/nlbone/adapters/http_clients → nlbone-0.6.8/src/nlbone/adapters/http_clients/uploadchi}/uploadchi.py +22 -46
- {nlbone-0.5.0/src/nlbone/adapters/http_clients → nlbone-0.6.8/src/nlbone/adapters/http_clients/uploadchi}/uploadchi_async.py +23 -23
- nlbone-0.6.8/src/nlbone/adapters/percolation/__init__.py +1 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/percolation/connection.py +2 -1
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/config/logging.py +54 -24
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/config/settings.py +20 -12
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/container.py +12 -6
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/application/base_worker.py +1 -1
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/domain/models.py +4 -2
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/ports/cache.py +25 -9
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/dependencies/auth.py +26 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/cli/init_db.py +1 -1
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/cli/main.py +6 -5
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/utils/cache.py +10 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/utils/cache_keys.py +6 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/utils/cache_registry.py +5 -2
- nlbone-0.6.8/src/nlbone/utils/http.py +29 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/utils/redactor.py +2 -1
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/utils/time.py +1 -1
- nlbone-0.5.0/src/nlbone/adapters/http_clients/email_gateway.py +0 -0
- nlbone-0.5.0/src/nlbone/adapters/percolation/__init__.py +0 -1
- nlbone-0.5.0/src/nlbone/interfaces/jobs/__init__.py +0 -0
- nlbone-0.5.0/src/nlbone/utils/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/.gitignore +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/LICENSE +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/README.md +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/auth/__init__.py +0 -0
- {nlbone-0.5.0/src/nlbone/adapters → nlbone-0.6.8/src/nlbone/adapters/cache}/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/postgres/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/postgres/base.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/postgres/engine.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/postgres/query_builder.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/postgres/repository.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/postgres/schema.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/db/postgres/uow.py +0 -0
- {nlbone-0.5.0/src/nlbone/adapters/cache → nlbone-0.6.8/src/nlbone/adapters/db/redis}/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/messaging/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/messaging/event_bus.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/adapters/messaging/redis.py +0 -0
- {nlbone-0.5.0/src/nlbone/adapters/db/redis → nlbone-0.6.8/src/nlbone/config}/__init__.py +0 -0
- {nlbone-0.5.0/src/nlbone/adapters/http_clients → nlbone-0.6.8/src/nlbone/core}/__init__.py +0 -0
- {nlbone-0.5.0/src/nlbone/config → nlbone-0.6.8/src/nlbone/core/application}/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/application/events.py +0 -0
- {nlbone-0.5.0/src/nlbone/core → nlbone-0.6.8/src/nlbone/core/application/services}/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/application/services.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/application/use_case.py +0 -0
- {nlbone-0.5.0/src/nlbone/core/application → nlbone-0.6.8/src/nlbone/core/domain}/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/domain/base.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/domain/events.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/ports/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/ports/auth.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/ports/event_bus.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/ports/files.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/ports/messaging.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/ports/repo.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/core/ports/uow.py +0 -0
- {nlbone-0.5.0/src/nlbone/core/application/services → nlbone-0.6.8/src/nlbone/interfaces}/__init__.py +0 -0
- {nlbone-0.5.0/src/nlbone/core/domain → nlbone-0.6.8/src/nlbone/interfaces/api}/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/dependencies/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/dependencies/async_auth.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/dependencies/uow.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/exception_handlers.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/exceptions.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/middleware/authentication.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/pagination/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/routers.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/api/schemas.py +0 -0
- {nlbone-0.5.0/src/nlbone/interfaces → nlbone-0.6.8/src/nlbone/interfaces/cli}/__init__.py +0 -0
- {nlbone-0.5.0/src/nlbone/interfaces/api → nlbone-0.6.8/src/nlbone/interfaces/jobs}/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/types.py +0 -0
- {nlbone-0.5.0/src/nlbone/interfaces/cli → nlbone-0.6.8/src/nlbone/utils}/__init__.py +0 -0
- {nlbone-0.5.0 → nlbone-0.6.8}/src/nlbone/utils/context.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nlbone"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.8"
|
|
8
8
|
description = "Backbone package for interfaces and infrastructure in Python projects"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -63,4 +63,4 @@ dev = [
|
|
|
63
63
|
]
|
|
64
64
|
|
|
65
65
|
[project.scripts]
|
|
66
|
-
nlbone = "nlbone.interfaces.cli.main:main"
|
|
66
|
+
nlbone = "nlbone.interfaces.cli.main:main"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import nlbone.adapters.db.postgres.audit
|
|
@@ -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 --------
|
|
@@ -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",
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .pricing_service import CalculatePriceIn, CalculatePriceOut, PricingService
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from typing import List, Literal, Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
import requests
|
|
6
|
+
from pydantic import BaseModel, Field, NonNegativeInt, RootModel
|
|
7
|
+
|
|
8
|
+
from nlbone.adapters.auth.token_provider import ClientTokenProvider
|
|
9
|
+
from nlbone.config.settings import get_settings
|
|
10
|
+
from nlbone.utils.http import auth_headers, normalize_https_base
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PricingError(Exception):
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class CalculatePriceIn(BaseModel):
|
|
18
|
+
params: dict[str, str]
|
|
19
|
+
product_id: NonNegativeInt | None = None
|
|
20
|
+
product_title: str | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DiscountType(str, Enum):
|
|
24
|
+
percent = "percent"
|
|
25
|
+
amount = "amount"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Product(BaseModel):
|
|
29
|
+
id: Optional[int] = Field(None, description="Nullable product id")
|
|
30
|
+
service_product_id: NonNegativeInt
|
|
31
|
+
title: Optional[str] = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Pricing(BaseModel):
|
|
35
|
+
source: Literal["formula", "static"]
|
|
36
|
+
price: Optional[float] = None
|
|
37
|
+
discount: Optional[float] = None
|
|
38
|
+
discount_type: Optional[DiscountType] = None
|
|
39
|
+
params: Optional[dict] = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class Segment(BaseModel):
|
|
43
|
+
id: str
|
|
44
|
+
name: str
|
|
45
|
+
specificity: int
|
|
46
|
+
matched_fields: list
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Formula(BaseModel):
|
|
50
|
+
id: int
|
|
51
|
+
title: str
|
|
52
|
+
key: str
|
|
53
|
+
status: str
|
|
54
|
+
description: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class PricingRule(BaseModel):
|
|
58
|
+
product: Product
|
|
59
|
+
segment: Segment | None
|
|
60
|
+
formula: Optional[Formula] = None
|
|
61
|
+
pricing: Pricing
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class CalculatePriceOut(RootModel[List[PricingRule]]):
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PricingService:
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
token_provider: ClientTokenProvider,
|
|
72
|
+
base_url: Optional[str] = None,
|
|
73
|
+
timeout_seconds: Optional[float] = None,
|
|
74
|
+
client: httpx.Client | None = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
s = get_settings()
|
|
77
|
+
self._base_url = normalize_https_base(base_url or str(s.PRICING_SERVICE_URL), enforce_https=False)
|
|
78
|
+
self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
|
|
79
|
+
self._client = client or requests.session()
|
|
80
|
+
self._token_provider = token_provider
|
|
81
|
+
|
|
82
|
+
def calculate(self, items: list[CalculatePriceIn], response: Literal["list", "dict"] = "dict") -> CalculatePriceOut:
|
|
83
|
+
body = {"items": [i.model_dump() for i in items]}
|
|
84
|
+
|
|
85
|
+
r = self._client.post(
|
|
86
|
+
f"{self._base_url}/price/calculate",
|
|
87
|
+
params={"response": response},
|
|
88
|
+
headers=auth_headers(self._token_provider.get_access_token()),
|
|
89
|
+
json=body,
|
|
90
|
+
timeout=self._timeout,
|
|
91
|
+
verify=False,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if r.status_code not in (200, 204):
|
|
95
|
+
raise PricingError(r.status_code, r.text)
|
|
96
|
+
|
|
97
|
+
if r.status_code == 204 or not r.content:
|
|
98
|
+
return CalculatePriceOut.model_validate(root=[])
|
|
99
|
+
|
|
100
|
+
return CalculatePriceOut.model_validate(r.json())
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
from typing import Any, Optional
|
|
5
|
-
from urllib.parse import urlparse, urlunparse
|
|
6
4
|
|
|
7
5
|
import httpx
|
|
8
6
|
import requests
|
|
@@ -10,6 +8,7 @@ import requests
|
|
|
10
8
|
from nlbone.adapters.auth.token_provider import ClientTokenProvider
|
|
11
9
|
from nlbone.config.settings import get_settings
|
|
12
10
|
from nlbone.core.ports.files import FileServicePort
|
|
11
|
+
from nlbone.utils.http import auth_headers, build_list_query, normalize_https_base
|
|
13
12
|
|
|
14
13
|
|
|
15
14
|
class UploadchiError(RuntimeError):
|
|
@@ -26,21 +25,6 @@ def _resolve_token(explicit: str | None) -> str | None:
|
|
|
26
25
|
return s.UPLOADCHI_TOKEN.get_secret_value() if s.UPLOADCHI_TOKEN else None
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
def _auth_headers(token: str | None) -> dict[str, str]:
|
|
30
|
-
return {"Authorization": f"Bearer {token}"} if token else {}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
def _build_list_query(
|
|
34
|
-
limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
|
|
35
|
-
) -> dict[str, Any]:
|
|
36
|
-
q: dict[str, Any] = {"limit": limit, "offset": offset}
|
|
37
|
-
if filters:
|
|
38
|
-
q["filters"] = json.dumps(filters)
|
|
39
|
-
if sort:
|
|
40
|
-
q["sort"] = ",".join([f"{f}:{o}" for f, o in sort])
|
|
41
|
-
return q
|
|
42
|
-
|
|
43
|
-
|
|
44
28
|
def _filename_from_cd(cd: str | None, fallback: str) -> str:
|
|
45
29
|
if not cd:
|
|
46
30
|
return fallback
|
|
@@ -49,24 +33,16 @@ def _filename_from_cd(cd: str | None, fallback: str) -> str:
|
|
|
49
33
|
return fallback
|
|
50
34
|
|
|
51
35
|
|
|
52
|
-
def _normalize_https_base(url: str) -> str:
|
|
53
|
-
p = urlparse(url.strip())
|
|
54
|
-
p = p._replace(scheme="https") # enforce https
|
|
55
|
-
if p.path.endswith("/"):
|
|
56
|
-
p = p._replace(path=p.path.rstrip("/"))
|
|
57
|
-
return str(urlunparse(p))
|
|
58
|
-
|
|
59
|
-
|
|
60
36
|
class UploadchiClient(FileServicePort):
|
|
61
37
|
def __init__(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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,
|
|
67
43
|
) -> None:
|
|
68
44
|
s = get_settings()
|
|
69
|
-
self._base_url =
|
|
45
|
+
self._base_url = normalize_https_base(base_url or str(s.UPLOADCHI_BASE_URL))
|
|
70
46
|
self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
|
|
71
47
|
self._client = client or requests.session()
|
|
72
48
|
self._token_provider = token_provider
|
|
@@ -75,12 +51,12 @@ class UploadchiClient(FileServicePort):
|
|
|
75
51
|
self._client.close()
|
|
76
52
|
|
|
77
53
|
def upload_file(
|
|
78
|
-
|
|
54
|
+
self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
|
|
79
55
|
) -> dict:
|
|
80
56
|
tok = _resolve_token(token)
|
|
81
57
|
files = {"file": (filename, file_bytes)}
|
|
82
58
|
data = (params or {}).copy()
|
|
83
|
-
r = self._client.post(self._base_url, files=files, data=data, headers=
|
|
59
|
+
r = self._client.post(self._base_url, files=files, data=data, headers=auth_headers(tok))
|
|
84
60
|
if r.status_code >= 400:
|
|
85
61
|
raise UploadchiError(r.status_code, r.text)
|
|
86
62
|
return r.json()
|
|
@@ -91,7 +67,7 @@ class UploadchiClient(FileServicePort):
|
|
|
91
67
|
tok = _resolve_token(token)
|
|
92
68
|
r = self._client.post(
|
|
93
69
|
f"{self._base_url}/{file_id}/commit",
|
|
94
|
-
headers=
|
|
70
|
+
headers=auth_headers(tok or self._token_provider.get_access_token()),
|
|
95
71
|
)
|
|
96
72
|
if r.status_code not in (204, 200):
|
|
97
73
|
raise UploadchiError(r.status_code, r.text)
|
|
@@ -102,36 +78,36 @@ class UploadchiClient(FileServicePort):
|
|
|
102
78
|
tok = _resolve_token(token)
|
|
103
79
|
r = self._client.post(
|
|
104
80
|
f"{self._base_url}/{file_id}/rollback",
|
|
105
|
-
headers=
|
|
81
|
+
headers=auth_headers(tok or self._token_provider.get_access_token()),
|
|
106
82
|
)
|
|
107
83
|
if r.status_code not in (204, 200):
|
|
108
84
|
raise UploadchiError(r.status_code, r.text)
|
|
109
85
|
|
|
110
86
|
def list_files(
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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,
|
|
117
93
|
) -> dict:
|
|
118
94
|
tok = _resolve_token(token)
|
|
119
|
-
q =
|
|
120
|
-
r = self._client.get(self._base_url, params=q, headers=
|
|
95
|
+
q = build_list_query(limit, offset, filters, sort)
|
|
96
|
+
r = self._client.get(self._base_url, params=q, headers=auth_headers(tok))
|
|
121
97
|
if r.status_code >= 400:
|
|
122
98
|
raise UploadchiError(r.status_code, r.text)
|
|
123
99
|
return r.json()
|
|
124
100
|
|
|
125
101
|
def get_file(self, file_id: str, token: str | None = None) -> dict:
|
|
126
102
|
tok = _resolve_token(token)
|
|
127
|
-
r = self._client.get(f"{self._base_url}/{file_id}", headers=
|
|
103
|
+
r = self._client.get(f"{self._base_url}/{file_id}", headers=auth_headers(tok))
|
|
128
104
|
if r.status_code >= 400:
|
|
129
105
|
raise UploadchiError(r.status_code, r.text)
|
|
130
106
|
return r.json()
|
|
131
107
|
|
|
132
108
|
def download_file(self, file_id: str, token: str | None = None) -> tuple[bytes, str, str]:
|
|
133
109
|
tok = _resolve_token(token)
|
|
134
|
-
r = self._client.get(f"{self._base_url}/{file_id}/download", headers=
|
|
110
|
+
r = self._client.get(f"{self._base_url}/{file_id}/download", headers=auth_headers(tok))
|
|
135
111
|
if r.status_code >= 400:
|
|
136
112
|
raise UploadchiError(r.status_code, r.text)
|
|
137
113
|
filename = _filename_from_cd(r.headers.get("content-disposition"), fallback=f"file-{file_id}")
|
|
@@ -140,6 +116,6 @@ class UploadchiClient(FileServicePort):
|
|
|
140
116
|
|
|
141
117
|
def delete_file(self, file_id: str, token: str | None = None) -> None:
|
|
142
118
|
tok = _resolve_token(token)
|
|
143
|
-
r = self._client.delete(f"{self._base_url}/{file_id}", headers=
|
|
119
|
+
r = self._client.delete(f"{self._base_url}/{file_id}", headers=auth_headers(tok))
|
|
144
120
|
if r.status_code not in (204, 200):
|
|
145
121
|
raise UploadchiError(r.status_code, r.text)
|