nlbone 0.9.7__py3-none-any.whl → 0.11.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nlbone/adapters/auth/__init__.py +2 -0
- nlbone/adapters/auth/async_auth_service.py +110 -0
- nlbone/adapters/auth/async_token_provider.py +49 -0
- nlbone/adapters/http_clients/pricing/async_pricing_service.py +66 -0
- nlbone/adapters/http_clients/pricing/pricing_service.py +6 -9
- nlbone/adapters/http_clients/uploadchi/uploadchi.py +7 -7
- nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +28 -11
- nlbone/config/settings.py +3 -1
- nlbone/container.py +11 -2
- nlbone/core/ports/auth.py +12 -0
- nlbone/interfaces/api/dependencies/__init__.py +1 -1
- nlbone/interfaces/api/dependencies/async_auth.py +59 -15
- nlbone/interfaces/api/dependencies/auth.py +2 -2
- nlbone/interfaces/api/middleware/authentication.py +44 -48
- {nlbone-0.9.7.dist-info → nlbone-0.11.0.dist-info}/METADATA +1 -1
- {nlbone-0.9.7.dist-info → nlbone-0.11.0.dist-info}/RECORD +19 -16
- {nlbone-0.9.7.dist-info → nlbone-0.11.0.dist-info}/WHEEL +0 -0
- {nlbone-0.9.7.dist-info → nlbone-0.11.0.dist-info}/entry_points.txt +0 -0
- {nlbone-0.9.7.dist-info → nlbone-0.11.0.dist-info}/licenses/LICENSE +0 -0
nlbone/adapters/auth/__init__.py
CHANGED
|
@@ -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}"
|
|
@@ -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)
|
|
@@ -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"))
|
nlbone/config/settings.py
CHANGED
|
@@ -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
|
# ---------------------------
|
nlbone/container.py
CHANGED
|
@@ -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,
|
nlbone/core/ports/auth.py
CHANGED
|
@@ -9,3 +9,15 @@ class AuthService(Protocol):
|
|
|
9
9
|
def is_client_token(self, token: str, allowed_clients: set[str] | None = None) -> bool: ...
|
|
10
10
|
def client_has_access(self, token: str, perms: list[str], allowed_clients: set[str] | None = None) -> bool: ...
|
|
11
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]: ...
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
from .async_auth import client_has_access, current_request, current_user_id, has_access, user_authenticated
|
|
2
1
|
from .auth import ( # noqa: F811
|
|
3
2
|
client_has_access,
|
|
4
3
|
current_client_id,
|
|
5
4
|
current_request,
|
|
6
5
|
current_user_id,
|
|
7
6
|
has_access,
|
|
7
|
+
is_permitted_user,
|
|
8
8
|
user_authenticated,
|
|
9
9
|
)
|
|
10
10
|
from .db import get_async_session, get_session
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
|
|
3
|
-
from nlbone.adapters.auth.
|
|
4
|
-
from nlbone.interfaces.api.exceptions import UnauthorizedException
|
|
3
|
+
from nlbone.adapters.auth.async_auth_service import get_async_auth_service
|
|
4
|
+
from nlbone.interfaces.api.exceptions import ForbiddenException, UnauthorizedException
|
|
5
5
|
from nlbone.utils.context import current_request
|
|
6
6
|
|
|
7
|
-
from .auth import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
async def current_user_id() -> int:
|
|
11
|
-
user_id = current_request().state.user_id
|
|
12
|
-
if user_id is not None:
|
|
13
|
-
return int(user_id)
|
|
14
|
-
raise UnauthorizedException()
|
|
7
|
+
from .auth import bypass_authz, current_user_id
|
|
15
8
|
|
|
16
9
|
|
|
17
10
|
async def current_client_id() -> str:
|
|
18
11
|
request = current_request()
|
|
19
|
-
if client_id :=
|
|
12
|
+
if client_id := await get_async_auth_service().get_client_id(request.state.token):
|
|
20
13
|
return str(client_id)
|
|
21
14
|
raise UnauthorizedException()
|
|
22
15
|
|
|
23
16
|
|
|
17
|
+
async def client_has_access_func(*, permissions=None):
|
|
18
|
+
request = current_request()
|
|
19
|
+
if not await get_async_auth_service().client_has_access(request.state.token, permissions=permissions):
|
|
20
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
|
|
24
24
|
def client_has_access(*, permissions=None):
|
|
25
25
|
def decorator(func):
|
|
26
26
|
@functools.wraps(func)
|
|
27
27
|
async def wrapper(*args, **kwargs):
|
|
28
|
-
client_has_access_func(permissions=permissions)
|
|
28
|
+
await client_has_access_func(permissions=permissions)
|
|
29
29
|
return await func(*args, **kwargs)
|
|
30
30
|
|
|
31
31
|
return wrapper
|
|
@@ -36,18 +36,29 @@ def client_has_access(*, permissions=None):
|
|
|
36
36
|
def user_authenticated(func):
|
|
37
37
|
@functools.wraps(func)
|
|
38
38
|
async def wrapper(*args, **kwargs):
|
|
39
|
-
if not
|
|
39
|
+
if not current_user_id():
|
|
40
40
|
raise UnauthorizedException()
|
|
41
41
|
return await func(*args, **kwargs)
|
|
42
42
|
|
|
43
43
|
return wrapper
|
|
44
44
|
|
|
45
45
|
|
|
46
|
+
async def user_has_access_func(*, permissions=None):
|
|
47
|
+
request = current_request()
|
|
48
|
+
if not current_user_id():
|
|
49
|
+
raise UnauthorizedException()
|
|
50
|
+
if bypass_authz():
|
|
51
|
+
return True
|
|
52
|
+
if not await get_async_auth_service().has_access(request.state.token, permissions=permissions):
|
|
53
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
54
|
+
return True
|
|
55
|
+
|
|
56
|
+
|
|
46
57
|
def has_access(*, permissions=None):
|
|
47
58
|
def decorator(func):
|
|
48
59
|
@functools.wraps(func)
|
|
49
60
|
async def wrapper(*args, **kwargs):
|
|
50
|
-
user_has_access_func(permissions=permissions)
|
|
61
|
+
await user_has_access_func(permissions=permissions)
|
|
51
62
|
return await func(*args, **kwargs)
|
|
52
63
|
|
|
53
64
|
return wrapper
|
|
@@ -55,13 +66,46 @@ def has_access(*, permissions=None):
|
|
|
55
66
|
return decorator
|
|
56
67
|
|
|
57
68
|
|
|
69
|
+
async def client_or_user_has_access_func(permissions=None, client_permissions=None):
|
|
70
|
+
if bypass_authz():
|
|
71
|
+
return True
|
|
72
|
+
request = current_request()
|
|
73
|
+
token = getattr(request.state, "token", None)
|
|
74
|
+
if not token:
|
|
75
|
+
raise UnauthorizedException()
|
|
76
|
+
needed = client_permissions or permissions
|
|
77
|
+
try:
|
|
78
|
+
await client_has_access_func(permissions=needed)
|
|
79
|
+
except Exception:
|
|
80
|
+
await user_has_access_func(permissions=needed)
|
|
81
|
+
|
|
82
|
+
|
|
58
83
|
def client_or_user_has_access(*, permissions=None, client_permissions=None):
|
|
59
84
|
def decorator(func):
|
|
60
85
|
@functools.wraps(func)
|
|
61
86
|
async def wrapper(*args, **kwargs):
|
|
62
|
-
client_or_user_has_access_func(permissions=permissions, client_permissions=client_permissions)
|
|
87
|
+
await client_or_user_has_access_func(permissions=permissions, client_permissions=client_permissions)
|
|
63
88
|
return await func(*args, **kwargs)
|
|
64
89
|
|
|
65
90
|
return wrapper
|
|
66
91
|
|
|
67
92
|
return decorator
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
async def is_permitted_user(permissions=None):
|
|
96
|
+
async def check_permissions():
|
|
97
|
+
try:
|
|
98
|
+
if bypass_authz():
|
|
99
|
+
return True
|
|
100
|
+
request = current_request()
|
|
101
|
+
if not current_user_id():
|
|
102
|
+
raise UnauthorizedException()
|
|
103
|
+
if not await get_async_auth_service().has_access(request.state.token, permissions=permissions):
|
|
104
|
+
raise ForbiddenException(f"Forbidden {permissions}")
|
|
105
|
+
return True
|
|
106
|
+
except ForbiddenException:
|
|
107
|
+
return False
|
|
108
|
+
except UnauthorizedException:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
return check_permissions
|
|
@@ -7,9 +7,9 @@ from nlbone.interfaces.api.exceptions import ForbiddenException, UnauthorizedExc
|
|
|
7
7
|
from nlbone.utils.context import current_request
|
|
8
8
|
|
|
9
9
|
|
|
10
|
-
@functools.lru_cache()
|
|
10
|
+
@functools.lru_cache(maxsize=1)
|
|
11
11
|
def bypass_authz() -> bool:
|
|
12
|
-
if get_settings().ENV
|
|
12
|
+
if get_settings().ENV not in ("prod", "staging"):
|
|
13
13
|
return True
|
|
14
14
|
return False
|
|
15
15
|
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
from typing import Callable, Optional, Union
|
|
1
|
+
from typing import Any, Callable, Optional, Union
|
|
2
2
|
|
|
3
3
|
from fastapi import Request
|
|
4
4
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
5
5
|
|
|
6
|
-
from nlbone.adapters.auth.auth_service import AuthService
|
|
7
6
|
from nlbone.config.settings import get_settings
|
|
8
7
|
from nlbone.core.domain.models import CurrentUserData
|
|
8
|
+
from nlbone.core.ports.auth import AsyncAuthService as BaseAuthService
|
|
9
9
|
|
|
10
10
|
try:
|
|
11
11
|
from dependency_injector import providers
|
|
12
12
|
|
|
13
13
|
ProviderType = providers.Provider # type: ignore
|
|
14
|
-
except
|
|
14
|
+
except ImportError:
|
|
15
15
|
ProviderType = object
|
|
16
16
|
|
|
17
|
-
from nlbone.core.ports.auth import AuthService as BaseAuthService
|
|
18
|
-
|
|
19
17
|
|
|
20
18
|
def _to_factory(auth: Union[BaseAuthService, Callable[[], BaseAuthService], ProviderType]):
|
|
21
19
|
try:
|
|
@@ -25,50 +23,43 @@ def _to_factory(auth: Union[BaseAuthService, Callable[[], BaseAuthService], Prov
|
|
|
25
23
|
return auth
|
|
26
24
|
except Exception:
|
|
27
25
|
pass
|
|
28
|
-
if callable(auth)
|
|
26
|
+
if callable(auth):
|
|
29
27
|
return auth
|
|
30
28
|
return lambda: auth
|
|
31
29
|
|
|
32
30
|
|
|
33
|
-
def
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if authz:
|
|
31
|
+
def _extract_token(request: Request) -> Optional[str]:
|
|
32
|
+
auth_header = request.headers.get("Authorization")
|
|
33
|
+
if auth_header:
|
|
37
34
|
try:
|
|
38
|
-
scheme, token =
|
|
39
|
-
if scheme.lower()
|
|
40
|
-
token
|
|
35
|
+
scheme, token = auth_header.split(" ", 1)
|
|
36
|
+
if scheme.lower() == "bearer":
|
|
37
|
+
return token
|
|
41
38
|
except ValueError:
|
|
42
|
-
token = None
|
|
43
|
-
|
|
44
|
-
if token:
|
|
45
|
-
request.state.token = token
|
|
46
|
-
try:
|
|
47
|
-
service: BaseAuthService = auth_service()
|
|
48
|
-
data = service.verify_token(token)
|
|
49
|
-
if data:
|
|
50
|
-
request.state.user_id = data.get("user_id")
|
|
51
|
-
except Exception:
|
|
52
39
|
pass
|
|
53
40
|
|
|
41
|
+
return request.cookies.get("access_token") or request.cookies.get("j_token")
|
|
54
42
|
|
|
55
|
-
def authenticate_user(request):
|
|
56
|
-
token = (
|
|
57
|
-
request.cookies.get("access_token") or request.cookies.get("j_token") or request.headers.get("Authorization")
|
|
58
|
-
)
|
|
59
|
-
if request.headers.get("Authorization"):
|
|
60
|
-
scheme, token = request.headers.get("Authorization").split(" ", 1)
|
|
61
43
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
44
|
+
async def authenticate_request(request: Request, auth_factory: Callable[[], BaseAuthService]) -> None:
|
|
45
|
+
token = _extract_token(request)
|
|
46
|
+
if not token:
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
request.state.token = token
|
|
50
|
+
try:
|
|
51
|
+
auth_service = auth_factory()
|
|
52
|
+
data = await auth_service.verify_token(token)
|
|
53
|
+
|
|
54
|
+
if data:
|
|
55
|
+
request.state.user_id = data.get("sub") or data.get("user_id")
|
|
56
|
+
|
|
57
|
+
try:
|
|
68
58
|
request.state.user = CurrentUserData.from_dict(data)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
except Exception:
|
|
60
|
+
pass
|
|
61
|
+
except Exception:
|
|
62
|
+
pass
|
|
72
63
|
|
|
73
64
|
|
|
74
65
|
class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
@@ -76,20 +67,25 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
|
|
|
76
67
|
super().__init__(app)
|
|
77
68
|
self._get_auth = _to_factory(auth)
|
|
78
69
|
|
|
79
|
-
async def dispatch(self, request: Request, call_next):
|
|
70
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Any:
|
|
80
71
|
request.state.client_id = None
|
|
81
72
|
request.state.user_id = None
|
|
82
73
|
request.state.token = None
|
|
83
74
|
request.state.user = None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
75
|
+
|
|
76
|
+
client_id = request.headers.get("X-Client-Id")
|
|
77
|
+
api_key = request.headers.get("X-Api-Key")
|
|
78
|
+
|
|
79
|
+
if client_id and api_key:
|
|
80
|
+
if api_key == get_settings().PRICING_API_SECRET:
|
|
81
|
+
request.state.client_id = client_id
|
|
87
82
|
return await call_next(request)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
request.headers.get("Authorization")
|
|
86
|
+
or request.cookies.get("access_token")
|
|
87
|
+
or request.cookies.get("j_token")
|
|
88
|
+
):
|
|
89
|
+
await authenticate_request(request, self._get_auth)
|
|
94
90
|
|
|
95
91
|
return await call_next(request)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
nlbone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
nlbone/container.py,sha256=
|
|
2
|
+
nlbone/container.py,sha256=c1frCaCwfV5ImOexyeX2vJYou0ZvuItPlQimtCZZxeA,3403
|
|
3
3
|
nlbone/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
4
|
nlbone/adapters/__init__.py,sha256=NzUmk4XPyp3GJOw7VSE86xkQMZLtG3MrOoXLeoB551M,41
|
|
5
5
|
nlbone/adapters/snowflake.py,sha256=eC5eXWgkTIJlO5J44VFbD1-MXj8HYs0lCNp37paSfXY,2324
|
|
6
|
-
nlbone/adapters/auth/__init__.py,sha256=
|
|
6
|
+
nlbone/adapters/auth/__init__.py,sha256=v2yU52Eq0pQQgFLC9BedyyWbu9xp_JCbsmjLGNj8Tvs,171
|
|
7
|
+
nlbone/adapters/auth/async_auth_service.py,sha256=gnz7GqJvoHYJFoIs444jm7XWadQkMYS3S1UMOxoUgdE,3722
|
|
8
|
+
nlbone/adapters/auth/async_token_provider.py,sha256=HVAuInFG483IZzrQ-agqOhiRiu4ix8kynwvcOniYr2E,1567
|
|
7
9
|
nlbone/adapters/auth/auth_service.py,sha256=CYfOX5M19SGsteHOTNiRLFohaVXx5X5NwmKQI2WLjCQ,2621
|
|
8
10
|
nlbone/adapters/auth/keycloak.py,sha256=IhEriaFl5mjIGT6ZUCU9qROd678ARchvWgd4UJ6zH7s,4925
|
|
9
11
|
nlbone/adapters/auth/token_provider.py,sha256=kzjFAaFY8SPnU0Tn6l-YVrhEOAiFV0QE3eit3D7u2VQ,1438
|
|
@@ -26,10 +28,11 @@ nlbone/adapters/db/redis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
|
|
|
26
28
|
nlbone/adapters/db/redis/client.py,sha256=cT9TCigE-ndB-ckcpuh8cTS1AOVKS3rWNG_WYxD0CAM,930
|
|
27
29
|
nlbone/adapters/http_clients/__init__.py,sha256=w-Yr9CLuXMU71N0Ada5HbvP1DB53wqeP6B-i5rALlTo,150
|
|
28
30
|
nlbone/adapters/http_clients/pricing/__init__.py,sha256=ElA9NFcAR9u4cqb_w3PPqKU3xGeyjNLQ8veJ0ql2iz0,81
|
|
29
|
-
nlbone/adapters/http_clients/pricing/
|
|
31
|
+
nlbone/adapters/http_clients/pricing/async_pricing_service.py,sha256=6vwVQ8SGTxFMOKhqlri8CQzzL5RgmDHI4icBiUATdXA,2846
|
|
32
|
+
nlbone/adapters/http_clients/pricing/pricing_service.py,sha256=TPDoeryibAXZndXOoREGTQfRh4lm4K45Cv4ng4gpwD4,3710
|
|
30
33
|
nlbone/adapters/http_clients/uploadchi/__init__.py,sha256=uBzEOuVtY22teWW2b36Pitkdk5yVdSqa6xbg22JfTNg,105
|
|
31
|
-
nlbone/adapters/http_clients/uploadchi/uploadchi.py,sha256=
|
|
32
|
-
nlbone/adapters/http_clients/uploadchi/uploadchi_async.py,sha256=
|
|
34
|
+
nlbone/adapters/http_clients/uploadchi/uploadchi.py,sha256=zSvUkDDL1m-OuCD-oJtqxNBghmK9yFWKpXi7t5-11-M,4933
|
|
35
|
+
nlbone/adapters/http_clients/uploadchi/uploadchi_async.py,sha256=onCf2Jm6LskB3vqFTPhM4pgi-zJmmcmBVbo4vOSffwY,5363
|
|
33
36
|
nlbone/adapters/i18n/__init__.py,sha256=fS97TR7HEc7fiDC2ufQKoFOxXDNkGA4njAFIB3EmhLk,426
|
|
34
37
|
nlbone/adapters/i18n/engine.py,sha256=yH_b614oJQ2PgM8qpQ5_Prroi4Jjqb-xPguHCCJm0_0,1305
|
|
35
38
|
nlbone/adapters/i18n/loaders.py,sha256=7td0Cmn0ehjDO2S2qeH31zwl8UyWI7quzedP9PnPhWk,2381
|
|
@@ -47,7 +50,7 @@ nlbone/adapters/ticketing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
|
|
|
47
50
|
nlbone/adapters/ticketing/client.py,sha256=b9U3ouK8sIVq1A_1Z6PgAEaT0_-a3PIP7DvRv-WaoAU,1384
|
|
48
51
|
nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
49
52
|
nlbone/config/logging.py,sha256=Ot6Ctf7EQZlW8YNB-uBdleqI6wixn5fH0Eo6QRgNkQk,4358
|
|
50
|
-
nlbone/config/settings.py,sha256=
|
|
53
|
+
nlbone/config/settings.py,sha256=oFkBsLX955JJkIZwL5d8b5LjKg6mrL75Db9wYsHHc6Y,5436
|
|
51
54
|
nlbone/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
55
|
nlbone/core/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
53
56
|
nlbone/core/application/base_worker.py,sha256=5brIToSd-vi6tw0ukhHnUZGZhOLq1SQ-NRRy-kp6D24,1193
|
|
@@ -60,7 +63,7 @@ nlbone/core/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuF
|
|
|
60
63
|
nlbone/core/domain/base.py,sha256=dz3gwNnOk048zLghbNNM_Wa0qPXLoRmH5w72fwtXNFM,2592
|
|
61
64
|
nlbone/core/domain/models.py,sha256=HSgICH4_0kPyZKGIawCd6VyJ5w6ZFXkSymBgVu79peI,4131
|
|
62
65
|
nlbone/core/ports/__init__.py,sha256=syJg3fAjQALD5Rjfm9wi9bQpkIvNTWjE9AURBmy587o,132
|
|
63
|
-
nlbone/core/ports/auth.py,sha256=
|
|
66
|
+
nlbone/core/ports/auth.py,sha256=RhRKFcO5NFfvhKJkHb9anSxpai5w6D1N4Fhy-bGPWV0,1114
|
|
64
67
|
nlbone/core/ports/cache.py,sha256=8pP_z4ta7PNNG8UiSrEF4xMZRm2wLPxISZvdPt7QnxQ,2351
|
|
65
68
|
nlbone/core/ports/event_bus.py,sha256=7iC8WRBg-EmcKJx7AVPkP-r823SLKGuDxGp9WF4q-_U,824
|
|
66
69
|
nlbone/core/ports/files.py,sha256=7Ov2ITYRpPwwDTZGCeNVISg8e3A9l08jbOgpTImgfK8,1863
|
|
@@ -80,16 +83,16 @@ nlbone/interfaces/api/additional_filed/field_registry.py,sha256=IhIvzHWOMtKv8iTd
|
|
|
80
83
|
nlbone/interfaces/api/additional_filed/resolver.py,sha256=jv1TIBBHN4LBIMwHGipcy4iq0uP0r6udyaqvhRzb8Bk,4655
|
|
81
84
|
nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py,sha256=LUSAOO3xRUt5ptlraIx7H-7dSkdr1D-WprmnqXRB16g,48
|
|
82
85
|
nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py,sha256=ecKqPeXZ-YiF14RK9PmK7ln3PCzpCUc18S5zm5IF3fw,339
|
|
83
|
-
nlbone/interfaces/api/dependencies/__init__.py,sha256=
|
|
84
|
-
nlbone/interfaces/api/dependencies/async_auth.py,sha256=
|
|
85
|
-
nlbone/interfaces/api/dependencies/auth.py,sha256=
|
|
86
|
+
nlbone/interfaces/api/dependencies/__init__.py,sha256=nrmQftdCfKlqSE44R6PkcxtkwCdNpZgI8yLlHyeiACA,274
|
|
87
|
+
nlbone/interfaces/api/dependencies/async_auth.py,sha256=c6PohIprT35konFbHQht0y0MJHBouhKqIK_XFQ9Rcbg,3465
|
|
88
|
+
nlbone/interfaces/api/dependencies/auth.py,sha256=4L-hspOyv9HpaCO-rAi7rk52PlZTgMClEG2LA2tMriM,3836
|
|
86
89
|
nlbone/interfaces/api/dependencies/client_credential.py,sha256=Bo4dYx75Qw0JzTKD9ZfV5EXDEOuwndJk2D-V37K2ePg,1293
|
|
87
90
|
nlbone/interfaces/api/dependencies/db.py,sha256=-UD39J_86UU7ZJs2ZncpdND0yhAG0NeeeALrgSDuuFw,466
|
|
88
91
|
nlbone/interfaces/api/dependencies/uow.py,sha256=QfLEvLYLNWZJQN1k-0q0hBVtUld3D75P4j39q_RjcnE,1181
|
|
89
92
|
nlbone/interfaces/api/middleware/__init__.py,sha256=zbX2vaEAfxRMIYwO2MVY_2O6bqG5H9o7HqGpX14U3Is,158
|
|
90
93
|
nlbone/interfaces/api/middleware/access_log.py,sha256=vIkxxxfy2HcjqqKb8XCfGCcSrivAC8u6ie75FMq5x-U,1032
|
|
91
94
|
nlbone/interfaces/api/middleware/add_request_context.py,sha256=o8mdo-D6fODM9OyHunE5UodkVxsh4F__5tDv8ju8Sxg,1952
|
|
92
|
-
nlbone/interfaces/api/middleware/authentication.py,sha256=
|
|
95
|
+
nlbone/interfaces/api/middleware/authentication.py,sha256=tF5tXUE8Ln14LLx6Hkx5AlTHpOxDwrN0UfkkgvV87ug,2810
|
|
93
96
|
nlbone/interfaces/api/pagination/__init__.py,sha256=pA1uC4rK6eqDI5IkLVxmgO2B6lExnOm8Pje2-hifJZw,431
|
|
94
97
|
nlbone/interfaces/api/pagination/offset_base.py,sha256=pdfNgmP99eFC5qCWyY1JgW8hNhOuEGnmrlvQPGArdj8,4709
|
|
95
98
|
nlbone/interfaces/api/schema/__init__.py,sha256=LAqgynfupeqOQ6u0I5ucrcYnojRMZUg9yW8IjKSQTNI,119
|
|
@@ -116,8 +119,8 @@ nlbone/utils/normalize_mobile.py,sha256=sGH4tV9gX-6eVKozviNWJhm1DN1J28Nj-ERldCYk
|
|
|
116
119
|
nlbone/utils/read_files.py,sha256=mx8dfvtaaARQFRp_U7OOiERg-GT62h09_lpTzIQsVhs,291
|
|
117
120
|
nlbone/utils/redactor.py,sha256=-V4HrHmHwPi3Kez587Ek1uJlgK35qGSrwBOvcbw8Jas,1279
|
|
118
121
|
nlbone/utils/time.py,sha256=DjjyQ9GLsfXoT6NK8RDW2rOlJg3e6sF04Jw6PBUrSvg,1268
|
|
119
|
-
nlbone-0.
|
|
120
|
-
nlbone-0.
|
|
121
|
-
nlbone-0.
|
|
122
|
-
nlbone-0.
|
|
123
|
-
nlbone-0.
|
|
122
|
+
nlbone-0.11.0.dist-info/METADATA,sha256=tCSglsRxWG-yPYxEmv_M9TMoZqsq_ixuNCgXXK7jJBM,2295
|
|
123
|
+
nlbone-0.11.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
124
|
+
nlbone-0.11.0.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
|
|
125
|
+
nlbone-0.11.0.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
126
|
+
nlbone-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|