nlbone 0.9.7__tar.gz → 0.11.1__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.9.7 → nlbone-0.11.1}/PKG-INFO +1 -1
- {nlbone-0.9.7 → nlbone-0.11.1}/pyproject.toml +1 -1
- nlbone-0.11.1/src/nlbone/adapters/auth/__init__.py +3 -0
- nlbone-0.11.1/src/nlbone/adapters/auth/async_auth_service.py +110 -0
- nlbone-0.11.1/src/nlbone/adapters/auth/async_token_provider.py +49 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/query_builder.py +52 -1
- nlbone-0.11.1/src/nlbone/adapters/http_clients/pricing/async_pricing_service.py +66 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/http_clients/pricing/pricing_service.py +6 -9
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/http_clients/uploadchi/uploadchi.py +7 -7
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +28 -11
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/config/settings.py +3 -1
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/container.py +11 -2
- nlbone-0.11.1/src/nlbone/core/ports/auth.py +23 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/additional_filed/assembler.py +50 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/additional_filed/field_registry.py +2 -1
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/additional_filed/resolver.py +79 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/dependencies/__init__.py +1 -1
- nlbone-0.11.1/src/nlbone/interfaces/api/dependencies/async_auth.py +111 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/dependencies/auth.py +2 -2
- nlbone-0.11.1/src/nlbone/interfaces/api/middleware/authentication.py +91 -0
- nlbone-0.9.7/src/nlbone/adapters/auth/__init__.py +0 -1
- nlbone-0.9.7/src/nlbone/core/ports/auth.py +0 -11
- nlbone-0.9.7/src/nlbone/interfaces/api/dependencies/async_auth.py +0 -67
- nlbone-0.9.7/src/nlbone/interfaces/api/middleware/authentication.py +0 -95
- {nlbone-0.9.7 → nlbone-0.11.1}/.gitignore +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/LICENSE +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/README.md +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/auth/auth_service.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/auth/keycloak.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/auth/token_provider.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/cache/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/cache/async_redis.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/cache/memory.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/cache/pubsub_listener.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/cache/redis.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/audit.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/base.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/engine.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/repository.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/schema.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/types.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/postgres/uow.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/redis/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/db/redis/client.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/http_clients/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/http_clients/pricing/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/http_clients/uploadchi/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/i18n/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/i18n/engine.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/i18n/loaders.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/i18n/locales/fa-IR.json +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/messaging/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/messaging/event_bus.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/messaging/rabbitmq.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/outbox/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/outbox/outbox_consumer.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/outbox/outbox_repo.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/percolation/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/percolation/connection.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/repositories/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/snowflake.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/ticketing/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/ticketing/client.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/config/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/config/logging.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/application/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/application/base_worker.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/application/bus.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/application/di.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/application/registry.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/application/services/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/application/use_case.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/domain/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/domain/base.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/domain/models.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/ports/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/ports/cache.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/ports/event_bus.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/ports/files.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/ports/outbox.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/ports/repository.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/ports/translation.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/core/ports/uow.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/additional_filed/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/dependencies/client_credential.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/dependencies/uow.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/exception_handlers.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/exceptions.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/pagination/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/routers.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/schema/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/schema/adaptive_schema.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/schema/base_response_model.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/api/schemas.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/cli/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/cli/crypto.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/cli/init_db.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/cli/main.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/cli/ticket.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/jobs/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/jobs/dispatch_outbox.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/types.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/__init__.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/cache.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/cache_keys.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/cache_registry.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/context.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/crypto.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/flatten_dict.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/flatten_sqlalchemy_result.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/http.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/normalize_mobile.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/read_files.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/redactor.py +0 -0
- {nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/utils/time.py +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
from functools import lru_cache
|
|
2
|
+
from typing import Any, Optional, Set
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from nlbone.config.settings import get_settings
|
|
7
|
+
from nlbone.core.ports.auth import AsyncAuthService as BaseAuthService
|
|
8
|
+
from nlbone.utils.cache import cached
|
|
9
|
+
from nlbone.utils.http import normalize_https_base
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncAuthService(BaseAuthService):
|
|
13
|
+
_client: Optional[httpx.AsyncClient] = None
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
s = get_settings()
|
|
17
|
+
self.client_id = s.CLIENT_ID or s.KEYCLOAK_CLIENT_ID
|
|
18
|
+
self.client_secret = s.CLIENT_SECRET.get_secret_value().strip()
|
|
19
|
+
self._base_url = normalize_https_base(s.AUTH_SERVICE_URL.unicode_string(), enforce_https=False)
|
|
20
|
+
self._timeout = float(s.HTTP_TIMEOUT_SECONDS)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_client(cls) -> httpx.AsyncClient:
|
|
24
|
+
if cls._client is None or cls._client.is_closed:
|
|
25
|
+
s = get_settings()
|
|
26
|
+
cls._client = httpx.AsyncClient(
|
|
27
|
+
timeout=float(s.HTTP_TIMEOUT_SECONDS),
|
|
28
|
+
limits=httpx.Limits(
|
|
29
|
+
max_keepalive_connections=s.HTTPX_MAX_KEEPALIVE_CONNECTIONS,
|
|
30
|
+
max_connections=s.HTTPX_MAX_CONNECTIONS,
|
|
31
|
+
),
|
|
32
|
+
)
|
|
33
|
+
return cls._client
|
|
34
|
+
|
|
35
|
+
@cached(ttl=15 * 60)
|
|
36
|
+
async def verify_token(self, token: str) -> Optional[dict[str, Any]]:
|
|
37
|
+
if not token:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
url = f"{self._base_url}/introspect"
|
|
41
|
+
client = self.get_client()
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
response = await client.post(url, data={"token": token})
|
|
45
|
+
if response.status_code == 200:
|
|
46
|
+
data = response.json()
|
|
47
|
+
if data.get("active") is True:
|
|
48
|
+
return data
|
|
49
|
+
except httpx.RequestError as e:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
async def has_access(self, token: str, permissions: list[str]) -> bool:
|
|
55
|
+
data = await self.verify_token(token)
|
|
56
|
+
if not data:
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
allowed = set(data.get("allowed_permissions", []))
|
|
60
|
+
required = {f"{self.client_id}#{perm}" for perm in permissions}
|
|
61
|
+
|
|
62
|
+
return required.issubset(allowed)
|
|
63
|
+
|
|
64
|
+
async def client_has_access(
|
|
65
|
+
self, token: str, permissions: list[str], allowed_clients: Set[str] | None = None
|
|
66
|
+
) -> bool:
|
|
67
|
+
return await self.has_access(token, permissions)
|
|
68
|
+
|
|
69
|
+
async def get_client_id(self, token: str) -> Optional[str]:
|
|
70
|
+
data = await self.verify_token(token)
|
|
71
|
+
if data:
|
|
72
|
+
username = data.get("preferred_username", "")
|
|
73
|
+
if username.startswith("service-account"):
|
|
74
|
+
return username
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
async def is_client_token(self, token: str, allowed_clients: Set[str] | None = None) -> bool:
|
|
78
|
+
data = await self.verify_token(token)
|
|
79
|
+
if data:
|
|
80
|
+
return data.get("preferred_username", "").startswith("service-account")
|
|
81
|
+
return False
|
|
82
|
+
|
|
83
|
+
async def get_client_token(self) -> dict | None:
|
|
84
|
+
url = f"{self._base_url}/token"
|
|
85
|
+
client = self.get_client()
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
result = await client.post(
|
|
89
|
+
url,
|
|
90
|
+
data={
|
|
91
|
+
"client_id": self.client_id,
|
|
92
|
+
"client_secret": self.client_secret,
|
|
93
|
+
"grant_type": "client_credentials",
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
if result.status_code == 200:
|
|
97
|
+
return result.json()
|
|
98
|
+
except httpx.RequestError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
async def get_permissions(self, token: str) -> list[str]:
|
|
104
|
+
data = await self.verify_token(token)
|
|
105
|
+
return data.get("allowed_permissions", []) if data else []
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@lru_cache(maxsize=1)
|
|
109
|
+
def get_async_auth_service() -> AsyncAuthService:
|
|
110
|
+
return AsyncAuthService()
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from nlbone.core.ports.auth import AsyncAuthService as BaseAuthService
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AsyncClientTokenProvider:
|
|
9
|
+
"""
|
|
10
|
+
Caches Keycloak client-credentials token and refreshes before expiry.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, auth: BaseAuthService, *, skew_seconds: int = 30) -> None:
|
|
14
|
+
self._auth = auth
|
|
15
|
+
self._skew = skew_seconds
|
|
16
|
+
self._lock = asyncio.Lock()
|
|
17
|
+
self._token: Optional[str] = None # access_token
|
|
18
|
+
self._expires_at: float = 0.0 # epoch seconds
|
|
19
|
+
|
|
20
|
+
def _needs_refresh(self) -> bool:
|
|
21
|
+
return not self._token or time.time() >= (self._expires_at - self._skew)
|
|
22
|
+
|
|
23
|
+
async def get_access_token(self) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Return a valid access token; refresh asynchronously if needed.
|
|
26
|
+
"""
|
|
27
|
+
if not self._needs_refresh() and self._token:
|
|
28
|
+
return self._token
|
|
29
|
+
|
|
30
|
+
async with self._lock:
|
|
31
|
+
if not self._needs_refresh() and self._token:
|
|
32
|
+
return self._token
|
|
33
|
+
|
|
34
|
+
data = await self._auth.get_client_token()
|
|
35
|
+
|
|
36
|
+
if not data or "access_token" not in data:
|
|
37
|
+
raise RuntimeError("Failed to retrieve access_token")
|
|
38
|
+
|
|
39
|
+
access_token = data["access_token"]
|
|
40
|
+
expires_in = int(data.get("expires_in", 15 * 60))
|
|
41
|
+
|
|
42
|
+
self._token = access_token
|
|
43
|
+
self._expires_at = time.time() + max(1, expires_in)
|
|
44
|
+
|
|
45
|
+
return self._token
|
|
46
|
+
|
|
47
|
+
async def get_auth_header(self) -> str:
|
|
48
|
+
token = await self.get_access_token()
|
|
49
|
+
return f"Bearer {token}"
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from typing import Any, Callable, List, Optional, Sequence, Type, Union
|
|
2
2
|
|
|
3
|
-
from sqlalchemy import and_, asc, case, desc, literal, or_
|
|
3
|
+
from sqlalchemy import Select, and_, asc, case, desc, func, literal, or_, select
|
|
4
4
|
from sqlalchemy.dialects.postgresql import ENUM as PGEnum
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
6
|
from sqlalchemy.orm import Query, Session, aliased
|
|
6
7
|
from sqlalchemy.orm.attributes import InstrumentedAttribute
|
|
7
8
|
from sqlalchemy.orm.interfaces import LoaderOption
|
|
@@ -384,6 +385,19 @@ def _apply_order(pagination: PaginateRequest, entity, query):
|
|
|
384
385
|
return query
|
|
385
386
|
|
|
386
387
|
|
|
388
|
+
def apply_pagination_async(pagination: PaginateRequest, entity, stmt: Select = None, limit: bool = True) -> Select:
|
|
389
|
+
if stmt is None:
|
|
390
|
+
stmt = select(entity)
|
|
391
|
+
|
|
392
|
+
stmt = _apply_filters(pagination, entity, stmt)
|
|
393
|
+
stmt = _apply_order(pagination, entity, stmt)
|
|
394
|
+
|
|
395
|
+
if limit:
|
|
396
|
+
stmt = stmt.limit(pagination.limit).offset(pagination.offset)
|
|
397
|
+
|
|
398
|
+
return stmt
|
|
399
|
+
|
|
400
|
+
|
|
387
401
|
def apply_pagination(pagination: PaginateRequest, entity, session: Session, limit=True, query=None) -> Query:
|
|
388
402
|
if not query:
|
|
389
403
|
query = session.query(entity)
|
|
@@ -437,6 +451,43 @@ def _serialize_item(item: Any, output_cls: OutputType) -> Any:
|
|
|
437
451
|
return item
|
|
438
452
|
|
|
439
453
|
|
|
454
|
+
async def get_paginated_response_async(
|
|
455
|
+
pagination,
|
|
456
|
+
entity,
|
|
457
|
+
session: AsyncSession,
|
|
458
|
+
*,
|
|
459
|
+
with_count: bool = True,
|
|
460
|
+
output_cls: Optional[Type] = None,
|
|
461
|
+
eager_options: Optional[Sequence[LoaderOption]] = None,
|
|
462
|
+
query: Optional[Select] = None,
|
|
463
|
+
) -> dict:
|
|
464
|
+
stmt = query if query is not None else select(entity)
|
|
465
|
+
|
|
466
|
+
if eager_options:
|
|
467
|
+
stmt = stmt.options(*eager_options)
|
|
468
|
+
|
|
469
|
+
filtered_stmt = apply_pagination_async(pagination, entity, stmt=stmt, limit=False)
|
|
470
|
+
|
|
471
|
+
total_count = None
|
|
472
|
+
if with_count:
|
|
473
|
+
subquery = filtered_stmt.subquery()
|
|
474
|
+
count_stmt = select(func.count()).select_from(subquery)
|
|
475
|
+
total_count = await session.scalar(count_stmt)
|
|
476
|
+
|
|
477
|
+
final_stmt = filtered_stmt.limit(pagination.limit).offset(pagination.offset)
|
|
478
|
+
result = await session.execute(final_stmt)
|
|
479
|
+
rows = result.scalars().all()
|
|
480
|
+
|
|
481
|
+
if output_cls is not None:
|
|
482
|
+
data = [output_cls.model_validate(r, from_attributes=True).model_dump() for r in rows]
|
|
483
|
+
else:
|
|
484
|
+
data = rows
|
|
485
|
+
|
|
486
|
+
return PaginateResponse(
|
|
487
|
+
total_count=total_count, data=data, limit=pagination.limit, offset=pagination.offset
|
|
488
|
+
).to_dict()
|
|
489
|
+
|
|
490
|
+
|
|
440
491
|
def get_paginated_response(
|
|
441
492
|
pagination,
|
|
442
493
|
entity,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import Dict, Literal, Optional
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from nlbone.adapters.auth.async_token_provider import AsyncClientTokenProvider
|
|
7
|
+
from nlbone.adapters.http_clients import CalculatePriceIn, CalculatePriceOut
|
|
8
|
+
from nlbone.adapters.http_clients.pricing.pricing_service import PricingError
|
|
9
|
+
from nlbone.config.settings import get_settings
|
|
10
|
+
from nlbone.utils.http import normalize_https_base
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AsyncPricingService:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
token_provider: AsyncClientTokenProvider,
|
|
17
|
+
base_url: Optional[str] = None,
|
|
18
|
+
timeout_seconds: Optional[float] = None,
|
|
19
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
20
|
+
) -> None:
|
|
21
|
+
s = get_settings()
|
|
22
|
+
self._token_provider = token_provider
|
|
23
|
+
self._base_url = normalize_https_base(base_url or str(s.PRICING_SERVICE_URL), enforce_https=False)
|
|
24
|
+
self._timeout = httpx.Timeout(timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS), connect=5.0)
|
|
25
|
+
self._client = client or httpx.AsyncClient(
|
|
26
|
+
timeout=self._timeout,
|
|
27
|
+
verify=True if s.ENV == "prod" else False,
|
|
28
|
+
limits=httpx.Limits(
|
|
29
|
+
max_keepalive_connections=s.HTTPX_MAX_KEEPALIVE_CONNECTIONS, max_connections=s.HTTPX_MAX_CONNECTIONS
|
|
30
|
+
),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
async def calculate(
|
|
34
|
+
self, items: list[CalculatePriceIn], response: Literal["list", "dict"] = "dict"
|
|
35
|
+
) -> CalculatePriceOut:
|
|
36
|
+
payload = {"items": [i.model_dump(mode="json") for i in items]}
|
|
37
|
+
|
|
38
|
+
response_obj = await self._client.post(
|
|
39
|
+
f"{self._base_url}/price/calculate",
|
|
40
|
+
params={"response": response},
|
|
41
|
+
headers={"X-Api-Key": get_settings().PRICING_API_SECRET, "X-Client-Id": get_settings().KEYCLOAK_CLIENT_ID},
|
|
42
|
+
# headers=auth_headers(await self._token_provider.get_access_token()),
|
|
43
|
+
json=payload,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
if response_obj.status_code not in (200, 204):
|
|
47
|
+
raise PricingError(response_obj.status_code, response_obj.text)
|
|
48
|
+
|
|
49
|
+
if response_obj.status_code == 204 or not response_obj.content:
|
|
50
|
+
return CalculatePriceOut.model_validate(root=[])
|
|
51
|
+
|
|
52
|
+
return CalculatePriceOut.model_validate(response_obj.json())
|
|
53
|
+
|
|
54
|
+
async def exchange_rates(self) -> Dict[str, Decimal]:
|
|
55
|
+
response_obj = await self._client.get(
|
|
56
|
+
f"{self._base_url}/variables/key/exchange_rates",
|
|
57
|
+
headers={"X-Api-Key": get_settings().PRICING_API_SECRET, "X-Client-Id": get_settings().KEYCLOAK_CLIENT_ID},
|
|
58
|
+
# headers=auth_headers(await self._token_provider.get_access_token()),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if response_obj.status_code != 200:
|
|
62
|
+
raise PricingError(response_obj.status_code, response_obj.text)
|
|
63
|
+
|
|
64
|
+
values = response_obj.json().get("values", [])
|
|
65
|
+
|
|
66
|
+
return {str(v["key"]): Decimal(str(v["value"])) for v in values}
|
|
@@ -76,11 +76,11 @@ class DecimalEncoder(json.JSONEncoder):
|
|
|
76
76
|
|
|
77
77
|
class PricingService:
|
|
78
78
|
def __init__(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
self,
|
|
80
|
+
token_provider: ClientTokenProvider,
|
|
81
|
+
base_url: Optional[str] = None,
|
|
82
|
+
timeout_seconds: Optional[float] = None,
|
|
83
|
+
client: httpx.Client | None = None,
|
|
84
84
|
) -> None:
|
|
85
85
|
s = get_settings()
|
|
86
86
|
self._base_url = normalize_https_base(base_url or str(s.PRICING_SERVICE_URL), enforce_https=False)
|
|
@@ -89,10 +89,7 @@ class PricingService:
|
|
|
89
89
|
self._token_provider = token_provider
|
|
90
90
|
|
|
91
91
|
def calculate(self, items: list[CalculatePriceIn], response: Literal["list", "dict"] = "dict") -> CalculatePriceOut:
|
|
92
|
-
body = json.dumps(
|
|
93
|
-
{"items": [i.model_dump() for i in items]},
|
|
94
|
-
cls=DecimalEncoder
|
|
95
|
-
)
|
|
92
|
+
body = json.dumps({"items": [i.model_dump() for i in items]}, cls=DecimalEncoder)
|
|
96
93
|
body = json.loads(body)
|
|
97
94
|
|
|
98
95
|
r = self._client.post(
|
|
@@ -56,7 +56,7 @@ class UploadchiClient(FileServicePort):
|
|
|
56
56
|
tok = _resolve_token(token)
|
|
57
57
|
files = {"file": (filename, file_bytes)}
|
|
58
58
|
data = (params or {}).copy()
|
|
59
|
-
r = self._client.post(self._base_url, files=files, data=data, headers=auth_headers(tok))
|
|
59
|
+
r = self._client.post(f"{self._base_url}/files", files=files, data=data, headers=auth_headers(tok))
|
|
60
60
|
if r.status_code >= 400:
|
|
61
61
|
raise UploadchiError(r.status_code, r.text)
|
|
62
62
|
return r.json()
|
|
@@ -66,7 +66,7 @@ class UploadchiClient(FileServicePort):
|
|
|
66
66
|
raise UploadchiError(detail="token_provider is not provided", status=400)
|
|
67
67
|
tok = _resolve_token(token)
|
|
68
68
|
r = self._client.post(
|
|
69
|
-
f"{self._base_url}/{file_id}/commit",
|
|
69
|
+
f"{self._base_url}/files/{file_id}/commit",
|
|
70
70
|
headers=auth_headers(tok or self._token_provider.get_access_token()),
|
|
71
71
|
)
|
|
72
72
|
if r.status_code not in (204, 200):
|
|
@@ -77,7 +77,7 @@ class UploadchiClient(FileServicePort):
|
|
|
77
77
|
raise UploadchiError(detail="token_provider is not provided", status=400)
|
|
78
78
|
tok = _resolve_token(token)
|
|
79
79
|
r = self._client.post(
|
|
80
|
-
f"{self._base_url}/{file_id}/rollback",
|
|
80
|
+
f"{self._base_url}/files/{file_id}/rollback",
|
|
81
81
|
headers=auth_headers(tok or self._token_provider.get_access_token()),
|
|
82
82
|
)
|
|
83
83
|
if r.status_code not in (204, 200):
|
|
@@ -93,21 +93,21 @@ class UploadchiClient(FileServicePort):
|
|
|
93
93
|
) -> dict:
|
|
94
94
|
tok = _resolve_token(token)
|
|
95
95
|
q = build_list_query(limit, offset, filters, sort)
|
|
96
|
-
r = self._client.get(self._base_url, params=q, headers=auth_headers(tok))
|
|
96
|
+
r = self._client.get(f"{self._base_url}/files", params=q, headers=auth_headers(tok))
|
|
97
97
|
if r.status_code >= 400:
|
|
98
98
|
raise UploadchiError(r.status_code, r.text)
|
|
99
99
|
return r.json()
|
|
100
100
|
|
|
101
101
|
def get_file(self, file_id: str, token: str | None = None) -> dict:
|
|
102
102
|
tok = _resolve_token(token)
|
|
103
|
-
r = self._client.get(f"{self._base_url}/{file_id}", headers=auth_headers(tok))
|
|
103
|
+
r = self._client.get(f"{self._base_url}/files/{file_id}", headers=auth_headers(tok))
|
|
104
104
|
if r.status_code >= 400:
|
|
105
105
|
raise UploadchiError(r.status_code, r.text)
|
|
106
106
|
return r.json()
|
|
107
107
|
|
|
108
108
|
def download_file(self, file_id: str, token: str | None = None) -> tuple[bytes, str, str]:
|
|
109
109
|
tok = _resolve_token(token)
|
|
110
|
-
r = self._client.get(f"{self._base_url}/{file_id}/download", headers=auth_headers(tok))
|
|
110
|
+
r = self._client.get(f"{self._base_url}/files/{file_id}/download", headers=auth_headers(tok))
|
|
111
111
|
if r.status_code >= 400:
|
|
112
112
|
raise UploadchiError(r.status_code, r.text)
|
|
113
113
|
filename = _filename_from_cd(r.headers.get("content-disposition"), fallback=f"file-{file_id}")
|
|
@@ -117,7 +117,7 @@ class UploadchiClient(FileServicePort):
|
|
|
117
117
|
def delete_file(self, file_id: str, token: str | None = None) -> None:
|
|
118
118
|
tok = _resolve_token(token)
|
|
119
119
|
r = self._client.delete(
|
|
120
|
-
f"{self._base_url}/{file_id}", headers=auth_headers(tok or self._token_provider.get_access_token())
|
|
120
|
+
f"{self._base_url}/files/{file_id}", headers=auth_headers(tok or self._token_provider.get_access_token())
|
|
121
121
|
)
|
|
122
122
|
if r.status_code not in (204, 200):
|
|
123
123
|
raise UploadchiError(r.status_code, r.text)
|
{nlbone-0.9.7 → nlbone-0.11.1}/src/nlbone/adapters/http_clients/uploadchi/uploadchi_async.py
RENAMED
|
@@ -4,7 +4,7 @@ from typing import Any, AsyncIterator, Optional
|
|
|
4
4
|
|
|
5
5
|
import httpx
|
|
6
6
|
|
|
7
|
-
from nlbone.adapters.auth.
|
|
7
|
+
from nlbone.adapters.auth.async_token_provider import AsyncClientTokenProvider
|
|
8
8
|
from nlbone.adapters.http_clients.uploadchi.uploadchi import UploadchiError, _filename_from_cd, _resolve_token
|
|
9
9
|
from nlbone.config.settings import get_settings
|
|
10
10
|
from nlbone.core.ports.files import AsyncFileServicePort
|
|
@@ -14,16 +14,21 @@ from nlbone.utils.http import auth_headers, build_list_query
|
|
|
14
14
|
class UploadchiAsyncClient(AsyncFileServicePort):
|
|
15
15
|
def __init__(
|
|
16
16
|
self,
|
|
17
|
-
token_provider:
|
|
17
|
+
token_provider: Optional[AsyncClientTokenProvider] = None,
|
|
18
18
|
base_url: Optional[str] = None,
|
|
19
19
|
timeout_seconds: Optional[float] = None,
|
|
20
|
-
client: httpx.AsyncClient
|
|
20
|
+
client: Optional[httpx.AsyncClient] = None,
|
|
21
21
|
) -> None:
|
|
22
22
|
s = get_settings()
|
|
23
23
|
self._base_url = base_url or str(s.UPLOADCHI_BASE_URL)
|
|
24
24
|
self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
|
|
25
25
|
self._client = client or httpx.AsyncClient(
|
|
26
|
-
base_url=self._base_url,
|
|
26
|
+
base_url=self._base_url,
|
|
27
|
+
timeout=self._timeout,
|
|
28
|
+
limits=httpx.Limits(
|
|
29
|
+
max_keepalive_connections=s.HTTPX_MAX_KEEPALIVE_CONNECTIONS, max_connections=s.HTTPX_MAX_CONNECTIONS
|
|
30
|
+
),
|
|
31
|
+
follow_redirects=True,
|
|
27
32
|
)
|
|
28
33
|
self._token_provider = token_provider
|
|
29
34
|
|
|
@@ -36,7 +41,9 @@ class UploadchiAsyncClient(AsyncFileServicePort):
|
|
|
36
41
|
tok = _resolve_token(token)
|
|
37
42
|
files = {"file": (filename, file_bytes)}
|
|
38
43
|
data = (params or {}).copy()
|
|
39
|
-
r = await self._client.post(
|
|
44
|
+
r = await self._client.post(
|
|
45
|
+
"/files", files=files, data=data, headers=auth_headers(tok or await self._token_provider.get_access_token())
|
|
46
|
+
)
|
|
40
47
|
if r.status_code >= 400:
|
|
41
48
|
raise UploadchiError(r.status_code, await r.aread())
|
|
42
49
|
return r.json()
|
|
@@ -46,7 +53,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
|
|
|
46
53
|
raise UploadchiError(detail="token_provider is not provided", status=400)
|
|
47
54
|
tok = _resolve_token(token)
|
|
48
55
|
r = await self._client.post(
|
|
49
|
-
f"/{file_id}/commit", headers=auth_headers(tok or self._token_provider.get_access_token())
|
|
56
|
+
f"/files/{file_id}/commit", headers=auth_headers(tok or await self._token_provider.get_access_token())
|
|
50
57
|
)
|
|
51
58
|
if r.status_code not in (204, 200):
|
|
52
59
|
raise UploadchiError(r.status_code, await r.aread())
|
|
@@ -56,7 +63,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
|
|
|
56
63
|
raise UploadchiError(detail="token_provider is not provided", status=400)
|
|
57
64
|
tok = _resolve_token(token)
|
|
58
65
|
r = await self._client.post(
|
|
59
|
-
f"/{file_id}/rollback", headers=auth_headers(tok or self._token_provider.get_access_token())
|
|
66
|
+
f"/files/{file_id}/rollback", headers=auth_headers(tok or await self._token_provider.get_access_token())
|
|
60
67
|
)
|
|
61
68
|
if r.status_code not in (204, 200):
|
|
62
69
|
raise UploadchiError(r.status_code, await r.aread())
|
|
@@ -71,21 +78,29 @@ class UploadchiAsyncClient(AsyncFileServicePort):
|
|
|
71
78
|
) -> dict:
|
|
72
79
|
tok = _resolve_token(token)
|
|
73
80
|
q = build_list_query(limit, offset, filters, sort)
|
|
74
|
-
r = await self._client.get(
|
|
81
|
+
r = await self._client.get(
|
|
82
|
+
"/files", params=q, headers=auth_headers(tok or await self._token_provider.get_access_token())
|
|
83
|
+
)
|
|
75
84
|
if r.status_code >= 400:
|
|
76
85
|
raise UploadchiError(r.status_code, await r.aread())
|
|
77
86
|
return r.json()
|
|
78
87
|
|
|
79
88
|
async def get_file(self, file_id: str, token: str | None = None) -> dict:
|
|
80
89
|
tok = _resolve_token(token)
|
|
81
|
-
r = await self._client.get(
|
|
90
|
+
r = await self._client.get(
|
|
91
|
+
f"/files/{file_id}", headers=auth_headers(tok or await self._token_provider.get_access_token())
|
|
92
|
+
)
|
|
82
93
|
if r.status_code >= 400:
|
|
83
94
|
raise UploadchiError(r.status_code, await r.aread())
|
|
84
95
|
return r.json()
|
|
85
96
|
|
|
86
97
|
async def download_file(self, file_id: str, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]:
|
|
87
98
|
tok = _resolve_token(token)
|
|
88
|
-
r = await self._client.get(
|
|
99
|
+
r = await self._client.get(
|
|
100
|
+
f"/files/{file_id}/download",
|
|
101
|
+
headers=auth_headers(tok or await self._token_provider.get_access_token()),
|
|
102
|
+
stream=True,
|
|
103
|
+
)
|
|
89
104
|
if r.status_code >= 400:
|
|
90
105
|
body = await r.aread()
|
|
91
106
|
raise UploadchiError(r.status_code, body.decode(errors="ignore"))
|
|
@@ -103,7 +118,9 @@ class UploadchiAsyncClient(AsyncFileServicePort):
|
|
|
103
118
|
|
|
104
119
|
async def delete_file(self, file_id: str, token: str | None = None) -> None:
|
|
105
120
|
tok = _resolve_token(token)
|
|
106
|
-
r = await self._client.delete(
|
|
121
|
+
r = await self._client.delete(
|
|
122
|
+
f"/files/{file_id}", headers=auth_headers(tok or await self._token_provider.get_access_token())
|
|
123
|
+
)
|
|
107
124
|
if r.status_code not in (204, 200):
|
|
108
125
|
body = await r.aread()
|
|
109
126
|
raise UploadchiError(r.status_code, body.decode(errors="ignore"))
|
|
@@ -52,6 +52,8 @@ class Settings(BaseSettings):
|
|
|
52
52
|
# HTTP / Timeouts
|
|
53
53
|
# ---------------------------
|
|
54
54
|
HTTP_TIMEOUT_SECONDS: float = Field(default=10.0)
|
|
55
|
+
HTTPX_MAX_KEEPALIVE_CONNECTIONS: int = Field(default=10)
|
|
56
|
+
HTTPX_MAX_CONNECTIONS: int = Field(default=30)
|
|
55
57
|
|
|
56
58
|
# ---------------------------
|
|
57
59
|
# Keycloak / Auth
|
|
@@ -104,7 +106,7 @@ class Settings(BaseSettings):
|
|
|
104
106
|
# ---------------------------
|
|
105
107
|
# UPLOADCHI
|
|
106
108
|
# ---------------------------
|
|
107
|
-
UPLOADCHI_BASE_URL: AnyHttpUrl = Field(default="https://uploadchi.numberland.ir/v1
|
|
109
|
+
UPLOADCHI_BASE_URL: AnyHttpUrl = Field(default="https://uploadchi.numberland.ir/v1")
|
|
108
110
|
UPLOADCHI_TOKEN: SecretStr = Field(default="")
|
|
109
111
|
|
|
110
112
|
# ---------------------------
|
|
@@ -5,6 +5,8 @@ from typing import Dict, Optional
|
|
|
5
5
|
from dependency_injector import containers, providers
|
|
6
6
|
from pydantic_settings import BaseSettings
|
|
7
7
|
|
|
8
|
+
from nlbone.adapters.auth.async_auth_service import AsyncAuthService as AsyncAuthService_IMP
|
|
9
|
+
from nlbone.adapters.auth.async_token_provider import AsyncClientTokenProvider
|
|
8
10
|
from nlbone.adapters.auth.auth_service import AuthService as AuthService_IMP
|
|
9
11
|
from nlbone.adapters.auth.token_provider import ClientTokenProvider
|
|
10
12
|
from nlbone.adapters.cache.async_redis import AsyncRedisCache
|
|
@@ -12,9 +14,10 @@ from nlbone.adapters.cache.memory import InMemoryCache
|
|
|
12
14
|
from nlbone.adapters.cache.redis import RedisCache
|
|
13
15
|
from nlbone.adapters.db.postgres.engine import get_async_session_factory, get_sync_session_factory
|
|
14
16
|
from nlbone.adapters.http_clients import PricingService
|
|
17
|
+
from nlbone.adapters.http_clients.pricing.async_pricing_service import AsyncPricingService
|
|
15
18
|
from nlbone.adapters.http_clients.uploadchi import UploadchiClient
|
|
16
19
|
from nlbone.adapters.http_clients.uploadchi.uploadchi_async import UploadchiAsyncClient
|
|
17
|
-
from nlbone.core.ports.auth import AuthService
|
|
20
|
+
from nlbone.core.ports.auth import AsyncAuthService, AuthService
|
|
18
21
|
from nlbone.core.ports.cache import AsyncCachePort, CachePort
|
|
19
22
|
from nlbone.core.ports.files import AsyncFileServicePort, FileServicePort
|
|
20
23
|
|
|
@@ -31,15 +34,21 @@ class Container(containers.DeclarativeContainer):
|
|
|
31
34
|
# --- Services ---
|
|
32
35
|
auth: providers.Singleton[AuthService] = providers.Singleton(AuthService_IMP)
|
|
33
36
|
token_provider = providers.Singleton(ClientTokenProvider, auth=auth, skew_seconds=30)
|
|
37
|
+
async_auth: providers.Singleton[AsyncAuthService] = providers.Singleton(AsyncAuthService_IMP)
|
|
38
|
+
async_token_provider = providers.Singleton(AsyncClientTokenProvider, auth=async_auth, skew_seconds=30)
|
|
39
|
+
|
|
34
40
|
file_service: providers.Singleton[FileServicePort] = providers.Singleton(
|
|
35
41
|
UploadchiClient, token_provider=token_provider
|
|
36
42
|
)
|
|
37
43
|
afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(
|
|
38
|
-
UploadchiAsyncClient, token_provider=
|
|
44
|
+
UploadchiAsyncClient, token_provider=async_token_provider
|
|
39
45
|
)
|
|
40
46
|
pricing_service: providers.Singleton[PricingService] = providers.Singleton(
|
|
41
47
|
PricingService, token_provider=token_provider
|
|
42
48
|
)
|
|
49
|
+
async_pricing_service: providers.Singleton[AsyncPricingService] = providers.Singleton(
|
|
50
|
+
AsyncPricingService, token_provider=async_token_provider
|
|
51
|
+
)
|
|
43
52
|
|
|
44
53
|
cache: providers.Singleton[CachePort] = providers.Selector(
|
|
45
54
|
config.CACHE_BACKEND,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Protocol, runtime_checkable
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@runtime_checkable
|
|
5
|
+
class AuthService(Protocol):
|
|
6
|
+
def has_access(self, token: str, permissions: list[str]) -> bool: ...
|
|
7
|
+
def verify_token(self, token: str) -> dict | None: ...
|
|
8
|
+
def get_client_token(self) -> dict | None: ...
|
|
9
|
+
def is_client_token(self, token: str, allowed_clients: set[str] | None = None) -> bool: ...
|
|
10
|
+
def client_has_access(self, token: str, perms: list[str], allowed_clients: set[str] | None = None) -> bool: ...
|
|
11
|
+
def get_permissions(self, token: str) -> list[str]: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class AsyncAuthService(Protocol):
|
|
16
|
+
async def has_access(self, token: str, permissions: list[str]) -> bool: ...
|
|
17
|
+
async def verify_token(self, token: str) -> dict | None: ...
|
|
18
|
+
async def get_client_token(self) -> dict | None: ...
|
|
19
|
+
async def is_client_token(self, token: str, allowed_clients: set[str] | None = None) -> bool: ...
|
|
20
|
+
async def client_has_access(
|
|
21
|
+
self, token: str, perms: list[str], allowed_clients: set[str] | None = None
|
|
22
|
+
) -> bool: ...
|
|
23
|
+
async def get_permissions(self, token: str) -> list[str]: ...
|
|
@@ -2,11 +2,61 @@ import inspect
|
|
|
2
2
|
from typing import Any, Dict
|
|
3
3
|
|
|
4
4
|
from pydantic import BaseModel
|
|
5
|
+
from sqlalchemy.ext.asyncio import AsyncSession
|
|
5
6
|
|
|
6
7
|
from nlbone.container import Container
|
|
7
8
|
from nlbone.interfaces.api.additional_filed.field_registry import FieldRule, ResourceRegistry
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
async def assemble_response_async(
|
|
12
|
+
obj: Any,
|
|
13
|
+
reg: ResourceRegistry,
|
|
14
|
+
selected_rules: Dict[str, FieldRule],
|
|
15
|
+
session: AsyncSession,
|
|
16
|
+
base_schema: type[BaseModel] | None,
|
|
17
|
+
scope_map: dict[str, set[str]] = None,
|
|
18
|
+
**kwargs,
|
|
19
|
+
) -> Dict[str, Any]:
|
|
20
|
+
"""
|
|
21
|
+
Async version of assemble_response.
|
|
22
|
+
Awaits loaders if they are coroutines.
|
|
23
|
+
"""
|
|
24
|
+
base = {f: getattr(obj, f, None) for f in reg.default_fields - set(reg.rules.keys())}
|
|
25
|
+
if base_schema:
|
|
26
|
+
base = base_schema.model_validate(base).model_dump()
|
|
27
|
+
|
|
28
|
+
ctx = {
|
|
29
|
+
"file_service": Container.afiles_service(),
|
|
30
|
+
"entity": obj,
|
|
31
|
+
"db": session,
|
|
32
|
+
"pricing_service": Container.async_pricing_service(),
|
|
33
|
+
**kwargs,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
roots = {name.split(".", 1)[0] for name in selected_rules.keys()}
|
|
37
|
+
|
|
38
|
+
for root in roots:
|
|
39
|
+
rule = reg.rules.get(root)
|
|
40
|
+
if not rule:
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
if rule.loader:
|
|
44
|
+
dependencies = ctx | {"scope": scope_map.get(root, {""})} if scope_map else ctx
|
|
45
|
+
|
|
46
|
+
result = inject_dependencies(rule.loader, dependencies=dependencies)
|
|
47
|
+
|
|
48
|
+
if inspect.iscoroutine(result):
|
|
49
|
+
value = await result
|
|
50
|
+
else:
|
|
51
|
+
value = result
|
|
52
|
+
else:
|
|
53
|
+
value = _get_nested_attr(obj, root)
|
|
54
|
+
|
|
55
|
+
_put_nested_key(base, root, value)
|
|
56
|
+
|
|
57
|
+
return base
|
|
58
|
+
|
|
59
|
+
|
|
10
60
|
def assemble_response(
|
|
11
61
|
obj: Any,
|
|
12
62
|
reg: ResourceRegistry,
|