nlbone 0.7.15__py3-none-any.whl → 0.7.16__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.
@@ -3,23 +3,57 @@ import requests
3
3
  from nlbone.config.settings import get_settings
4
4
  from nlbone.core.ports.auth import AuthService as BaseAuthService
5
5
  from nlbone.utils.http import normalize_https_base
6
+ from nlbone.utils.cache import cached
6
7
 
7
8
 
8
9
  class AuthService(BaseAuthService):
9
10
  def __init__(self):
10
11
  s = get_settings()
11
- self._base_url = normalize_https_base(s.AUTH_SERVICE_URL.unicode_string())
12
+ self.client_id = s.KEYCLOAK_CLIENT_ID
13
+ self.client_secret = s.CLIENT_SECRET.get_secret_value().strip()
14
+ self._base_url = normalize_https_base(s.AUTH_SERVICE_URL.unicode_string(), enforce_https=False)
12
15
  self._timeout = float(s.HTTP_TIMEOUT_SECONDS)
13
- self._client = requests.session()
16
+ self._client = requests.session()
14
17
 
15
- def has_access(self, token: str, permissions: list[str]) -> bool: ...
16
- def verify_token(self, token: str) -> dict | None:
18
+ def has_access(self, token: str, permissions: list[str]) -> bool:
19
+ ...
20
+
21
+ @cached(ttl=15 * 60)
22
+ def verify_token(self, token: str) -> dict:
17
23
  url = f"{self._base_url}/introspect"
18
24
  result = self._client.post(url, data={
19
25
  "token": token
20
26
  })
21
- return result.json()
22
- def get_client_token(self) -> dict | None: ...
23
- def is_client_token(self, token: str, allowed_clients: set[str] | None = None) -> bool: ...
24
- def client_has_access(self, token: str, perms: list[str], allowed_clients: set[str] | None = None) -> bool: ...
25
- def get_permissions(self, token: str) -> list[str]: ...
27
+ if result.status_code == 200:
28
+ return result.json()
29
+ return None
30
+
31
+ def get_client_id(self, token: str):
32
+ data = self.verify_token(token)
33
+ if data:
34
+ return data["sub"] if data["sub"].startswith('service-account') else None
35
+ return None
36
+
37
+ def get_client_token(self) -> dict | None:
38
+ url = f"{self._base_url}/introspect"
39
+ result = self._client.post(url, data={
40
+ "client_id": self.client_id,
41
+ "client_secret": self.client_secret,
42
+ "grant_type": "client_credentials"
43
+ })
44
+ if result.status_code == 200:
45
+ return result.json()
46
+ return None
47
+
48
+ def is_client_token(self, token: str, allowed_clients: set[str] | None = None) -> bool:
49
+ ...
50
+
51
+ def client_has_access(self, token: str, permissions: list[str], allowed_clients: set[str] | None = None) -> bool:
52
+ data = self.verify_token(token)
53
+ if not data:
54
+ return False
55
+ has_access = [perm in data.get("allowed_permissions", []) for perm in permissions]
56
+ return all(has_access)
57
+
58
+ def get_permissions(self, token: str) -> list[str]:
59
+ ...
@@ -31,7 +31,7 @@ class ClientTokenProvider:
31
31
  access_token = data.get("access_token")
32
32
  if not access_token:
33
33
  raise RuntimeError("Keycloak: missing access_token")
34
- expires_in = int(data.get("expires_in", 60))
34
+ expires_in = int(data.get("expires_in", 15 * 60))
35
35
  self._token = access_token
36
36
  self._expires_at = time.time() + max(1, expires_in)
37
37
  return self._token
nlbone/config/settings.py CHANGED
@@ -3,7 +3,7 @@ from functools import lru_cache
3
3
  from pathlib import Path
4
4
  from typing import Literal
5
5
 
6
- from pydantic import AnyHttpUrl, Field, SecretStr
6
+ from pydantic import AnyHttpUrl, Field, SecretStr, AliasChoices
7
7
  from pydantic_settings import BaseSettings, SettingsConfigDict
8
8
 
9
9
 
@@ -63,6 +63,8 @@ class Settings(BaseSettings):
63
63
  PRICING_API_SECRET: str = ''
64
64
 
65
65
  AUTH_SERVICE_URL: AnyHttpUrl = Field(default="https://auth.numberland.ir")
66
+ CLIENT_ID: str = Field(default="", validation_alias=AliasChoices("KEYCLOAK_CLIENT_ID"))
67
+ CLIENT_SECRET: SecretStr = Field(default="", validation_alias=AliasChoices("KEYCLOAK_CLIENT_SECRET"))
66
68
 
67
69
  # ---------------------------
68
70
  # Database
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.auth_service import AuthService as AuthService_IMP
9
+ from nlbone.core.ports.auth import AuthService
8
10
  from nlbone.adapters.auth.keycloak import KeycloakAuthService
9
11
  from nlbone.adapters.auth.token_provider import ClientTokenProvider
10
12
  from nlbone.adapters.cache.async_redis import AsyncRedisCache
@@ -28,7 +30,7 @@ class Container(containers.DeclarativeContainer):
28
30
  # event_bus: providers.Singleton[EventBusPort] = providers.Singleton(InMemoryEventBus)
29
31
 
30
32
  # --- Services ---
31
- auth: providers.Singleton[KeycloakAuthService] = providers.Singleton(KeycloakAuthService)
33
+ auth: providers.Singleton[AuthService] = providers.Singleton(AuthService_IMP)
32
34
  token_provider = providers.Singleton(ClientTokenProvider, auth=auth, skew_seconds=30)
33
35
  file_service: providers.Singleton[FileServicePort] = providers.Singleton(
34
36
  UploadchiClient, token_provider=token_provider
@@ -0,0 +1,44 @@
1
+ import asyncio
2
+ import functools
3
+ from typing import Callable
4
+
5
+ from makefun import wraps as mf_wraps
6
+
7
+ from nlbone.adapters.auth.auth_service import AuthService
8
+ from nlbone.interfaces.api.exceptions import UnauthorizedException, ForbiddenException
9
+ from nlbone.utils.context import current_request
10
+
11
+
12
+ def current_client_id() -> str:
13
+ request = current_request()
14
+ if client_id := AuthService().get_client_id(request.state.token):
15
+ return str(client_id)
16
+ raise UnauthorizedException()
17
+
18
+
19
+ def client_has_access_func(*, permissions=None):
20
+ request = current_request()
21
+ if not AuthService().client_has_access(request.state.token, permissions=permissions):
22
+ raise ForbiddenException(f"Forbidden {permissions}")
23
+ return True
24
+
25
+
26
+ def client_has_access(*, permissions=None):
27
+ def deco(func: Callable):
28
+ is_async_func = asyncio.iscoroutinefunction(func)
29
+
30
+ if is_async_func:
31
+ @mf_wraps(func)
32
+ async def aw(*args, **kwargs):
33
+ client_has_access_func(permissions=permissions)
34
+ return await func(*args, **kwargs)
35
+
36
+ return aw
37
+
38
+ @mf_wraps(func)
39
+ def sw(*args, **kwargs):
40
+ client_has_access_func(permissions=permissions)
41
+ return func(*args, **kwargs)
42
+
43
+ return sw
44
+ return deco
nlbone/utils/cache.py CHANGED
@@ -5,8 +5,7 @@ from typing import Any, Callable, Iterable, Optional, Mapping
5
5
  import hashlib
6
6
  from makefun import wraps as mf_wraps
7
7
 
8
- from nlbone.interfaces.api.additional_filed import AdditionalFieldsRequest
9
- from nlbone.interfaces.api.pagination import PaginateRequest
8
+
10
9
  from nlbone.utils.cache_registry import get_cache
11
10
 
12
11
  try:
@@ -31,6 +30,9 @@ PRIMITIVES = (str, int, float, bool, type(None))
31
30
 
32
31
 
33
32
  def to_jsonable(obj):
33
+ from nlbone.interfaces.api.additional_filed import AdditionalFieldsRequest
34
+ from nlbone.interfaces.api.pagination import PaginateRequest
35
+
34
36
  if isinstance(obj, PRIMITIVES):
35
37
  return obj
36
38
 
@@ -147,6 +149,8 @@ def cached(
147
149
  @mf_wraps(func)
148
150
  async def aw(*args, **kwargs):
149
151
  cache = (cache_resolver or get_cache)()
152
+ if not cache:
153
+ return await func(*args, **kwargs)
150
154
  k = _key_from_template(key, func, args, kwargs)
151
155
  tg = _format_tags(tags, func, args, kwargs)
152
156
 
@@ -177,6 +181,8 @@ def cached(
177
181
  @mf_wraps(func)
178
182
  def sw(*args, **kwargs):
179
183
  cache = (cache_resolver or get_cache)()
184
+ if not cache:
185
+ return func(*args, **kwargs)
180
186
  k = _key_from_template(key, func, args, kwargs)
181
187
  tg = _format_tags(tags, func, args, kwargs)
182
188
 
@@ -21,6 +21,4 @@ def set_context_cache_resolver(fn: Optional[Callable[[], T]]) -> None:
21
21
 
22
22
  def get_cache() -> T:
23
23
  fn = _ctx_resolver.get() or _global_resolver
24
- if fn is None:
25
- raise RuntimeError("Cache resolver not configured. Call set_cache_resolver(...) first.")
26
24
  return fn()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.7.15
3
+ Version: 0.7.16
4
4
  Summary: Backbone package for interfaces and infrastructure in Python projects
5
5
  Author-email: Amir Hosein Kahkbazzadeh <a.khakbazzadeh@gmail.com>
6
6
  License: MIT
@@ -1,12 +1,12 @@
1
1
  nlbone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- nlbone/container.py,sha256=I3Ev5L-CvfOquR4cyueIiplRriZ-_q_QlH_Xne9lA0k,2698
2
+ nlbone/container.py,sha256=2UQ1YDA6Y24u6FlOPiMRlZSBcj1hcCPrjqyLc7_0IQ0,2810
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=lmq7vi6HdX9hEuTW6BlTEAy91wmrA4Bx3tvGoagHTW4,2315
6
6
  nlbone/adapters/auth/__init__.py,sha256=hkDHvsFhw_UiOHG9ZSMqjiAhK4wumEforitveSZswVw,42
7
- nlbone/adapters/auth/auth_service.py,sha256=HoRaEOcab5DFmHi4yAudtwJDhaofRyqMVvnq65a3sqg,1062
7
+ nlbone/adapters/auth/auth_service.py,sha256=kwHyp2trsXeToyE5pb2CBLfyFLjwR_E8CsGgrOlrPT8,2090
8
8
  nlbone/adapters/auth/keycloak.py,sha256=IhEriaFl5mjIGT6ZUCU9qROd678ARchvWgd4UJ6zH7s,4925
9
- nlbone/adapters/auth/token_provider.py,sha256=vL2Hk6HXnBbpk40Tq1wpqak5QQ7KEQf3nRquT0N8V4Q,1433
9
+ nlbone/adapters/auth/token_provider.py,sha256=EcZ7nSXxPZJZGaWnyo3QDvrEbGdeXXWnhHnP1-kMniY,1438
10
10
  nlbone/adapters/cache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  nlbone/adapters/cache/async_redis.py,sha256=vvu5w4ANx0BVRHL95RAMGsD8CcaC-tSBMbCius2cuNc,6212
12
12
  nlbone/adapters/cache/memory.py,sha256=y8M4erHQXApiSMAqG6Qk4pxEb60hRdu1szPv6iqvO9c,3738
@@ -43,7 +43,7 @@ nlbone/adapters/ticketing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
43
43
  nlbone/adapters/ticketing/client.py,sha256=GAr0_4DVqVCDV7tT2gkxRyKIsvLwJRlT-xx8fsfOVJE,1303
44
44
  nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  nlbone/config/logging.py,sha256=Ot6Ctf7EQZlW8YNB-uBdleqI6wixn5fH0Eo6QRgNkQk,4358
46
- nlbone/config/settings.py,sha256=BABYoUBWXazg09bO4QNZDc2Dxg1onCx0hO72ETC4Yb0,4540
46
+ nlbone/config/settings.py,sha256=iP7ZtZUGNFwj5A8qGuqa0k6RSM8Z_sIpWI07zi3gIRM,4752
47
47
  nlbone/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
48
  nlbone/core/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
49
  nlbone/core/application/base_worker.py,sha256=5brIToSd-vi6tw0ukhHnUZGZhOLq1SQ-NRRy-kp6D24,1193
@@ -78,6 +78,7 @@ nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py,
78
78
  nlbone/interfaces/api/dependencies/__init__.py,sha256=rnYRrFVZCfICQrp_PVFlzNg3BeC57yM08wn2DbOHCfk,359
79
79
  nlbone/interfaces/api/dependencies/async_auth.py,sha256=FBa73JUE2D82k2P1k4daN4IdvZbrbCwREqPJKfYc-4Q,1710
80
80
  nlbone/interfaces/api/dependencies/auth.py,sha256=dKq3TIJN4CqKZcRIlMV3rz3v8sFgh9Rz_slIP2VNcvs,3083
81
+ nlbone/interfaces/api/dependencies/client_credential.py,sha256=kRmkWgqRoybXm-Mreiu-c0ve1oz7PlPzcsRTdyxjG94,1308
81
82
  nlbone/interfaces/api/dependencies/db.py,sha256=-UD39J_86UU7ZJs2ZncpdND0yhAG0NeeeALrgSDuuFw,466
82
83
  nlbone/interfaces/api/dependencies/uow.py,sha256=QfLEvLYLNWZJQN1k-0q0hBVtUld3D75P4j39q_RjcnE,1181
83
84
  nlbone/interfaces/api/middleware/__init__.py,sha256=zbX2vaEAfxRMIYwO2MVY_2O6bqG5H9o7HqGpX14U3Is,158
@@ -98,17 +99,17 @@ nlbone/interfaces/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
98
99
  nlbone/interfaces/jobs/dispatch_outbox.py,sha256=yLZSC3nvkgxT2LL4Pq_DYzCyf_tZB-FknrjjgN89GFg,809
99
100
  nlbone/interfaces/jobs/sync_tokens.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
101
  nlbone/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
101
- nlbone/utils/cache.py,sha256=3iJdPH896yF9iJfs_rGD-98CPamPOVCjn2LX0aGxFEA,7038
102
+ nlbone/utils/cache.py,sha256=BJoWbDicwTS1CF9m8sBKIEgfG-OszoQEEsMqrCHwNL0,7204
102
103
  nlbone/utils/cache_keys.py,sha256=Y2YSellHTbUOcoaNbl1jaD4r485VU_e4KXsfBWhYTBo,1075
103
- nlbone/utils/cache_registry.py,sha256=w28sEfUQZAhzCCqVH5TflWQY3nyDXyEcFWt8hkuHRHw,823
104
+ nlbone/utils/cache_registry.py,sha256=3FWYyhujW8oPBiVUPzk1CqJ3jJfxs9729Sbb1pQ5Fag,707
104
105
  nlbone/utils/context.py,sha256=MmclJ24BG2uvSTg1IK7J-Da9BhVFDQ5ag4Ggs2FF1_w,1600
105
106
  nlbone/utils/crypto.py,sha256=PX0Tlf2nqXcGbuv16J26MoUPzo2c4xcD4sZBXxhBXgQ,746
106
107
  nlbone/utils/http.py,sha256=MPDEyaC16AKsL0YH6sWCPp8NC2TgzEHpWERYK5HcaYQ,1001
107
108
  nlbone/utils/normalize_mobile.py,sha256=sGH4tV9gX-6eVKozviNWJhm1DN1J28Nj-ERldCYkS_E,732
108
109
  nlbone/utils/redactor.py,sha256=-V4HrHmHwPi3Kez587Ek1uJlgK35qGSrwBOvcbw8Jas,1279
109
110
  nlbone/utils/time.py,sha256=DjjyQ9GLsfXoT6NK8RDW2rOlJg3e6sF04Jw6PBUrSvg,1268
110
- nlbone-0.7.15.dist-info/METADATA,sha256=4vijLQahgvuIahwx3IeCaXnAb2i9QzbRfpaTUQh779Q,2295
111
- nlbone-0.7.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
112
- nlbone-0.7.15.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
113
- nlbone-0.7.15.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
- nlbone-0.7.15.dist-info/RECORD,,
111
+ nlbone-0.7.16.dist-info/METADATA,sha256=nI-Pv5HnUjZhrgj0UpnnRQEe6oj8yG59STiPRYrs1UA,2295
112
+ nlbone-0.7.16.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
113
+ nlbone-0.7.16.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
114
+ nlbone-0.7.16.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
115
+ nlbone-0.7.16.dist-info/RECORD,,