nlbone 0.7.2__py3-none-any.whl → 0.7.3__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.
@@ -0,0 +1,25 @@
1
+ import requests
2
+
3
+ from nlbone.config.settings import get_settings
4
+ from nlbone.core.ports.auth import AuthService as BaseAuthService
5
+ from nlbone.utils.http import normalize_https_base
6
+
7
+
8
+ class AuthService(BaseAuthService):
9
+ def __init__(self):
10
+ s = get_settings()
11
+ self._base_url = normalize_https_base(s.AUTH_SERVICE_URL.unicode_string())
12
+ self._timeout = float(s.HTTP_TIMEOUT_SECONDS)
13
+ self._client = requests.session()
14
+
15
+ def has_access(self, token: str, permissions: list[str]) -> bool: ...
16
+ def verify_token(self, token: str) -> dict | None:
17
+ url = f"{self._base_url}/introspect"
18
+ result = self._client.post(url, data={
19
+ "token": token
20
+ })
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]: ...
@@ -0,0 +1,21 @@
1
+ from sqlalchemy.types import TypeDecorator, BigInteger
2
+
3
+ class BaseIntegerIdType(TypeDecorator):
4
+ """Maps BaseId subclasses <-> BIGINT transparently (Infrastructure-only)."""
5
+ impl = BigInteger
6
+ cache_ok = True
7
+
8
+ def __init__(self, id_cls):
9
+ super().__init__()
10
+ self.id_cls = id_cls
11
+
12
+ def process_bind_param(self, value, dialect):
13
+ # Python -> DB
14
+ if value is None:
15
+ return None
16
+ return int(value)
17
+
18
+ def process_result_value(self, value, dialect):
19
+ if value is None:
20
+ return None
21
+ return self.id_cls(int(value))
@@ -0,0 +1,70 @@
1
+ import threading
2
+ import time
3
+ from datetime import datetime, timezone
4
+
5
+ from nlbone.config.settings import get_settings
6
+
7
+
8
+ class Snowflake:
9
+ WORKER_ID_BITS = 5
10
+ DATACENTER_ID_BITS = 5
11
+ SEQUENCE_BITS = 12
12
+
13
+ MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1 # 31
14
+ MAX_DATACENTER_ID = (1 << DATACENTER_ID_BITS) - 1 # 31
15
+ SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1 # 4095
16
+
17
+ WORKER_ID_SHIFT = SEQUENCE_BITS
18
+ DATACENTER_ID_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS
19
+ TIMESTAMP_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS
20
+
21
+ EPOCH = int(datetime(2020, 1, 1, tzinfo=timezone.utc).timestamp() * 1000)
22
+
23
+ def __init__(self, datacenter_id: int, worker_id: int):
24
+ if not (0 <= worker_id <= self.MAX_WORKER_ID):
25
+ raise ValueError(f"worker_id must be 0..{self.MAX_WORKER_ID}")
26
+ if not (0 <= datacenter_id <= self.MAX_DATACENTER_ID):
27
+ raise ValueError(f"datacenter_id must be 0..{self.MAX_DATACENTER_ID}")
28
+
29
+ self.datacenter_id = datacenter_id
30
+ self.worker_id = worker_id
31
+ self.sequence = 0
32
+ self.last_ts = -1
33
+ self._lock = threading.Lock()
34
+
35
+ @staticmethod
36
+ def _timestamp_ms() -> int:
37
+ return int(time.time() * 1000)
38
+
39
+ def _wait_next_ms(self, last_ts: int) -> int:
40
+ ts = self._timestamp_ms()
41
+ while ts <= last_ts:
42
+ ts = self._timestamp_ms()
43
+ return ts
44
+
45
+ def next_id(self) -> int:
46
+ with self._lock:
47
+ ts = self._timestamp_ms()
48
+
49
+ if ts < self.last_ts:
50
+ ts = self._wait_next_ms(self.last_ts)
51
+
52
+ if ts == self.last_ts:
53
+ self.sequence = (self.sequence + 1) & self.SEQUENCE_MASK
54
+ if self.sequence == 0:
55
+ ts = self._wait_next_ms(self.last_ts)
56
+ else:
57
+ self.sequence = 0
58
+
59
+ self.last_ts = ts
60
+
61
+ id64 = ((ts - self.EPOCH) << self.TIMESTAMP_SHIFT) | \
62
+ (self.datacenter_id << self.DATACENTER_ID_SHIFT) | \
63
+ (self.worker_id << self.WORKER_ID_SHIFT) | \
64
+ self.sequence
65
+ return id64
66
+
67
+ setting = get_settings()
68
+ _DC_ID = int(setting.SNOWFLAKE_DATACENTER_ID)
69
+ _WORKER_ID = int(setting.SNOWFLAKE_WORKER_ID)
70
+ SNOWFLAKE = Snowflake(datacenter_id=_DC_ID, worker_id=_WORKER_ID)
nlbone/config/settings.py CHANGED
@@ -43,6 +43,9 @@ class Settings(BaseSettings):
43
43
  LOG_LEVEL: Literal["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"] = Field(default="INFO")
44
44
  LOG_JSON: bool = Field(default=True)
45
45
 
46
+ SNOWFLAKE_WORKER_ID : int = Field(default=1)
47
+ SNOWFLAKE_DATACENTER_ID: int = Field(default=1)
48
+
46
49
  AUDIT_DEFAULT_ENABLE: bool = False
47
50
 
48
51
  # ---------------------------
@@ -58,6 +61,8 @@ class Settings(BaseSettings):
58
61
  KEYCLOAK_CLIENT_ID: str = Field(default="nlbone")
59
62
  KEYCLOAK_CLIENT_SECRET: SecretStr = Field(default=SecretStr("dev-secret"))
60
63
 
64
+ AUTH_SERVICE_URL: AnyHttpUrl = Field(default="https://auth.numberland.ir")
65
+
61
66
  # ---------------------------
62
67
  # Database
63
68
  # ---------------------------
@@ -3,11 +3,13 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from datetime import datetime, timezone
5
5
  from enum import Enum
6
- from typing import Any, Generic, List, TypeVar
6
+ from typing import Any, Generic, List, TypeVar, Callable, ClassVar
7
7
  from uuid import uuid4
8
8
 
9
9
  from pydantic import BaseModel
10
10
 
11
+ from nlbone.adapters.snowflake import SNOWFLAKE
12
+
11
13
  TId = TypeVar("TId")
12
14
 
13
15
 
@@ -70,3 +72,37 @@ class AggregateRoot(Entity[TId]):
70
72
 
71
73
  class BaseEnum(Enum):
72
74
  pass
75
+
76
+
77
+
78
+ ID = TypeVar("ID", bound="BaseId")
79
+
80
+ @dataclass(frozen=True)
81
+ class BaseId(ValueObject):
82
+ value: int
83
+
84
+ _generator: ClassVar[Callable[[], int]] = lambda: SNOWFLAKE.next_id()
85
+
86
+ def __post_init__(self):
87
+ if not isinstance(self.value, int):
88
+ raise TypeError("BaseId.value must be int")
89
+ if self.value <= 0:
90
+ raise ValueError("BaseId must be a positive integer")
91
+
92
+ def __int__(self) -> int:
93
+ return self.value
94
+
95
+ def __str__(self) -> str:
96
+ return str(self.value)
97
+
98
+ @classmethod
99
+ def set_generator(cls, gen: Callable[[], int]) -> None:
100
+ cls._generator = gen
101
+
102
+ @classmethod
103
+ def new(cls: type[ID]) -> ID:
104
+ return cls(cls._generator())
105
+
106
+ @classmethod
107
+ def from_int(cls: type[ID], v: int) -> ID:
108
+ return cls(v)
@@ -3,6 +3,8 @@ from typing import Callable, Optional, Union
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
8
  try:
7
9
  from dependency_injector import providers
8
10
 
@@ -10,10 +12,10 @@ try:
10
12
  except Exception:
11
13
  ProviderType = object
12
14
 
13
- from nlbone.core.ports.auth import AuthService
15
+ from nlbone.core.ports.auth import AuthService as BaseAuthService
14
16
 
15
17
 
16
- def _to_factory(auth: Union[AuthService, Callable[[], AuthService], ProviderType]):
18
+ def _to_factory(auth: Union[BaseAuthService, Callable[[], BaseAuthService], ProviderType]):
17
19
  try:
18
20
  from dependency_injector import providers as _p # type: ignore
19
21
 
@@ -26,8 +28,43 @@ def _to_factory(auth: Union[AuthService, Callable[[], AuthService], ProviderType
26
28
  return lambda: auth
27
29
 
28
30
 
31
+ def authenticate_admin_user(request, auth_service):
32
+ token: Optional[str] = None
33
+ authz = request.headers.get("Authorization")
34
+ if authz:
35
+ try:
36
+ scheme, token = authz.split(" ", 1)
37
+ if scheme.lower() != "bearer":
38
+ token = None
39
+ except ValueError:
40
+ token = None
41
+
42
+ if token:
43
+ request.state.token = token
44
+ try:
45
+ service: BaseAuthService = auth_service()
46
+ data = service.verify_token(token)
47
+ if data:
48
+ request.state.user_id = data.get("user_id")
49
+ except Exception:
50
+ pass
51
+
52
+ def authenticate_user(request):
53
+ token = request.cookies.get("access_token") or request.cookies.get("j_token")
54
+
55
+ if token:
56
+ request.state.token = token
57
+ try:
58
+ service: BaseAuthService = AuthService()
59
+ data = service.verify_token(token)
60
+ if data:
61
+ request.state.user_id = data.get("sub")
62
+ except Exception:
63
+ pass
64
+
65
+
29
66
  class AuthenticationMiddleware(BaseHTTPMiddleware):
30
- def __init__(self, app, auth: Union[AuthService, Callable[[], AuthService], ProviderType]):
67
+ def __init__(self, app, auth: Union[BaseAuthService, Callable[[], BaseAuthService], ProviderType]):
31
68
  super().__init__(app)
32
69
  self._get_auth = _to_factory(auth)
33
70
 
@@ -35,25 +72,9 @@ class AuthenticationMiddleware(BaseHTTPMiddleware):
35
72
  request.state.client_id = request.headers.get("X-Client-Id")
36
73
  request.state.user_id = None
37
74
  request.state.token = None
38
-
39
- token: Optional[str] = None
40
- authz = request.headers.get("Authorization")
41
- if authz:
42
- try:
43
- scheme, token = authz.split(" ", 1)
44
- if scheme.lower() != "bearer":
45
- token = None
46
- except ValueError:
47
- token = None
48
-
49
- if token:
50
- request.state.token = token
51
- try:
52
- service: AuthService = self._get_auth()
53
- data = service.verify_token(token)
54
- if data:
55
- request.state.user_id = data.get("user_id")
56
- except Exception:
57
- pass
75
+ if request.cookies.get("access_token"):
76
+ authenticate_user(request)
77
+ elif request.headers.get("Authorization"):
78
+ authenticate_admin_user(request, auth_service=self._get_auth)
58
79
 
59
80
  return await call_next(request)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.7.2
3
+ Version: 0.7.3
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
@@ -2,7 +2,9 @@ nlbone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  nlbone/container.py,sha256=I3Ev5L-CvfOquR4cyueIiplRriZ-_q_QlH_Xne9lA0k,2698
3
3
  nlbone/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  nlbone/adapters/__init__.py,sha256=NzUmk4XPyp3GJOw7VSE86xkQMZLtG3MrOoXLeoB551M,41
5
+ nlbone/adapters/snowflake.py,sha256=lmq7vi6HdX9hEuTW6BlTEAy91wmrA4Bx3tvGoagHTW4,2315
5
6
  nlbone/adapters/auth/__init__.py,sha256=hkDHvsFhw_UiOHG9ZSMqjiAhK4wumEforitveSZswVw,42
7
+ nlbone/adapters/auth/auth_service.py,sha256=HoRaEOcab5DFmHi4yAudtwJDhaofRyqMVvnq65a3sqg,1062
6
8
  nlbone/adapters/auth/keycloak.py,sha256=IhEriaFl5mjIGT6ZUCU9qROd678ARchvWgd4UJ6zH7s,4925
7
9
  nlbone/adapters/auth/token_provider.py,sha256=vL2Hk6HXnBbpk40Tq1wpqak5QQ7KEQf3nRquT0N8V4Q,1433
8
10
  nlbone/adapters/cache/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -18,6 +20,7 @@ nlbone/adapters/db/postgres/engine.py,sha256=UCegauVB1gvo42ThytYnn5VIcQBwR-5xhcX
18
20
  nlbone/adapters/db/postgres/query_builder.py,sha256=Qv_2oZ5OxZwtN3Ts-jaAX_8sLBzb1mpGBhlNF7aR6Wk,12543
19
21
  nlbone/adapters/db/postgres/repository.py,sha256=n01TAzdKd-UbOhirE6KMosuvRdJG2l1cszwVHjTM-Ks,10345
20
22
  nlbone/adapters/db/postgres/schema.py,sha256=NlE7Rr8uXypsw4oWkdZhZwcIBHQEPIpoHLxcUo98i6s,1039
23
+ nlbone/adapters/db/postgres/types.py,sha256=OXra45PL8Umrqj6Pul_8ZlYRGIWQE36jvBXxui_u6hs,600
21
24
  nlbone/adapters/db/postgres/uow.py,sha256=2vRp4RBkh9RVniEY6CMXNHt-XXK9W2CDNKR1upT5EJE,3788
22
25
  nlbone/adapters/db/redis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
26
  nlbone/adapters/db/redis/client.py,sha256=5SUnwP2-GrueSFimUbiqDvrQsumvIE2aeozk8l-vOfQ,466
@@ -40,7 +43,7 @@ nlbone/adapters/ticketing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
40
43
  nlbone/adapters/ticketing/client.py,sha256=V5_u7cxV67eAG4jj4vUD23VWGiMXIXSAU362pNS6hYU,1289
41
44
  nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
45
  nlbone/config/logging.py,sha256=Ot6Ctf7EQZlW8YNB-uBdleqI6wixn5fH0Eo6QRgNkQk,4358
43
- nlbone/config/settings.py,sha256=E67FOUgPH9prp5WQzGtOIDbVe0DTr_3WziBIh27SEvM,4324
46
+ nlbone/config/settings.py,sha256=o5kJBHuv-XKFOe_yQtkR0YrEbI2uZ89JpGjzDYhE7o4,4507
44
47
  nlbone/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
48
  nlbone/core/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
49
  nlbone/core/application/base_worker.py,sha256=5brIToSd-vi6tw0ukhHnUZGZhOLq1SQ-NRRy-kp6D24,1193
@@ -50,7 +53,7 @@ nlbone/core/application/registry.py,sha256=dTqV_4bkMsLJ60CesZuEel5xO36cD1qikiaCL
50
53
  nlbone/core/application/use_case.py,sha256=3GMQZ3CFK5cbLoBNBgohPft6GBq2j9_wr8iKRq_osQA,247
51
54
  nlbone/core/application/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
55
  nlbone/core/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
- nlbone/core/domain/base.py,sha256=6C_wsn4CfFlW7n6ACsyA4r-ZciF4xUv18y0LzhCqMq4,1434
56
+ nlbone/core/domain/base.py,sha256=EpjTDjSFV3or26_4qWh96JQ2hEIyStymhp16L9cuxJ4,2309
54
57
  nlbone/core/domain/models.py,sha256=VEIfzruBXcF5XrDrWEvh9q-Ba9-9x_JG20wZsBF5thc,3270
55
58
  nlbone/core/ports/__init__.py,sha256=syJg3fAjQALD5Rjfm9wi9bQpkIvNTWjE9AURBmy587o,132
56
59
  nlbone/core/ports/auth.py,sha256=C-GmUqHNx4bAku6KbW_OTpPXCEfurBWWyDi9KxpTi9M,553
@@ -80,7 +83,7 @@ nlbone/interfaces/api/dependencies/uow.py,sha256=QfLEvLYLNWZJQN1k-0q0hBVtUld3D75
80
83
  nlbone/interfaces/api/middleware/__init__.py,sha256=zbX2vaEAfxRMIYwO2MVY_2O6bqG5H9o7HqGpX14U3Is,158
81
84
  nlbone/interfaces/api/middleware/access_log.py,sha256=vIkxxxfy2HcjqqKb8XCfGCcSrivAC8u6ie75FMq5x-U,1032
82
85
  nlbone/interfaces/api/middleware/add_request_context.py,sha256=av-qs0biOYuF9R6RJOo2eYsFqDL9WRYWcjVakFhbt-w,1834
83
- nlbone/interfaces/api/middleware/authentication.py,sha256=ze7vCm492QsX9nPL6A-PqZCmC1C5ZgUE-OWI6fCLpsU,1809
86
+ nlbone/interfaces/api/middleware/authentication.py,sha256=NmCkN_PXBIMJ8tgKJU4C4JHq_9Ohe_-fwgiuv5uWlIk,2497
84
87
  nlbone/interfaces/api/pagination/__init__.py,sha256=pA1uC4rK6eqDI5IkLVxmgO2B6lExnOm8Pje2-hifJZw,431
85
88
  nlbone/interfaces/api/pagination/offset_base.py,sha256=65UgQZ70IlWUbP9Usd--IxVYW_V0hQLoyXHVffnLFZ0,3940
86
89
  nlbone/interfaces/api/schema/__init__.py,sha256=LAqgynfupeqOQ6u0I5ucrcYnojRMZUg9yW8IjKSQTNI,119
@@ -104,8 +107,8 @@ nlbone/utils/http.py,sha256=UXUoXgQdTRNT08ho8zl-C5ekfDsD8uf-JiMQ323ooqw,872
104
107
  nlbone/utils/normalize_mobile.py,sha256=sGH4tV9gX-6eVKozviNWJhm1DN1J28Nj-ERldCYkS_E,732
105
108
  nlbone/utils/redactor.py,sha256=-V4HrHmHwPi3Kez587Ek1uJlgK35qGSrwBOvcbw8Jas,1279
106
109
  nlbone/utils/time.py,sha256=DjjyQ9GLsfXoT6NK8RDW2rOlJg3e6sF04Jw6PBUrSvg,1268
107
- nlbone-0.7.2.dist-info/METADATA,sha256=KqXtF5AV-X4TaqT1Gouy9CUMa0JZAy0R2fq5a6zeBeA,2294
108
- nlbone-0.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
109
- nlbone-0.7.2.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
110
- nlbone-0.7.2.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
111
- nlbone-0.7.2.dist-info/RECORD,,
110
+ nlbone-0.7.3.dist-info/METADATA,sha256=oE6yopvG_cQ_3S7k4a2WqDnaMnDmH6ZiY4oYUiRzCRA,2294
111
+ nlbone-0.7.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
112
+ nlbone-0.7.3.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
113
+ nlbone-0.7.3.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
114
+ nlbone-0.7.3.dist-info/RECORD,,
File without changes