nlbone 0.9.6__tar.gz → 0.11.0__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.
Files changed (130) hide show
  1. {nlbone-0.9.6 → nlbone-0.11.0}/PKG-INFO +1 -1
  2. {nlbone-0.9.6 → nlbone-0.11.0}/pyproject.toml +1 -1
  3. nlbone-0.11.0/src/nlbone/adapters/auth/__init__.py +3 -0
  4. nlbone-0.11.0/src/nlbone/adapters/auth/async_auth_service.py +110 -0
  5. nlbone-0.11.0/src/nlbone/adapters/auth/async_token_provider.py +49 -0
  6. nlbone-0.11.0/src/nlbone/adapters/http_clients/pricing/async_pricing_service.py +66 -0
  7. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/http_clients/pricing/pricing_service.py +6 -9
  8. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/http_clients/uploadchi/uploadchi.py +7 -7
  9. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/http_clients/uploadchi/uploadchi_async.py +28 -11
  10. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/ticketing/client.py +5 -2
  11. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/config/settings.py +3 -1
  12. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/container.py +11 -2
  13. nlbone-0.11.0/src/nlbone/core/ports/auth.py +23 -0
  14. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/dependencies/__init__.py +1 -1
  15. nlbone-0.11.0/src/nlbone/interfaces/api/dependencies/async_auth.py +111 -0
  16. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/dependencies/auth.py +2 -2
  17. nlbone-0.11.0/src/nlbone/interfaces/api/middleware/authentication.py +91 -0
  18. nlbone-0.9.6/src/nlbone/adapters/auth/__init__.py +0 -1
  19. nlbone-0.9.6/src/nlbone/core/ports/auth.py +0 -11
  20. nlbone-0.9.6/src/nlbone/interfaces/api/dependencies/async_auth.py +0 -67
  21. nlbone-0.9.6/src/nlbone/interfaces/api/middleware/authentication.py +0 -95
  22. {nlbone-0.9.6 → nlbone-0.11.0}/.gitignore +0 -0
  23. {nlbone-0.9.6 → nlbone-0.11.0}/LICENSE +0 -0
  24. {nlbone-0.9.6 → nlbone-0.11.0}/README.md +0 -0
  25. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/__init__.py +0 -0
  26. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/__init__.py +0 -0
  27. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/auth/auth_service.py +0 -0
  28. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/auth/keycloak.py +0 -0
  29. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/auth/token_provider.py +0 -0
  30. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/cache/__init__.py +0 -0
  31. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/cache/async_redis.py +0 -0
  32. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/cache/memory.py +0 -0
  33. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/cache/pubsub_listener.py +0 -0
  34. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/cache/redis.py +0 -0
  35. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/__init__.py +0 -0
  36. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/__init__.py +0 -0
  37. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/audit.py +0 -0
  38. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/base.py +0 -0
  39. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/engine.py +0 -0
  40. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/query_builder.py +0 -0
  41. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/repository.py +0 -0
  42. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/schema.py +0 -0
  43. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/types.py +0 -0
  44. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/postgres/uow.py +0 -0
  45. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/redis/__init__.py +0 -0
  46. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/db/redis/client.py +0 -0
  47. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/http_clients/__init__.py +0 -0
  48. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/http_clients/pricing/__init__.py +0 -0
  49. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/http_clients/uploadchi/__init__.py +0 -0
  50. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/i18n/__init__.py +0 -0
  51. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/i18n/engine.py +0 -0
  52. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/i18n/loaders.py +0 -0
  53. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/i18n/locales/fa-IR.json +0 -0
  54. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/messaging/__init__.py +0 -0
  55. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/messaging/event_bus.py +0 -0
  56. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/messaging/rabbitmq.py +0 -0
  57. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/outbox/__init__.py +0 -0
  58. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/outbox/outbox_consumer.py +0 -0
  59. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/outbox/outbox_repo.py +0 -0
  60. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/percolation/__init__.py +0 -0
  61. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/percolation/connection.py +0 -0
  62. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/repositories/__init__.py +0 -0
  63. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/snowflake.py +0 -0
  64. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/adapters/ticketing/__init__.py +0 -0
  65. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/config/__init__.py +0 -0
  66. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/config/logging.py +0 -0
  67. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/__init__.py +0 -0
  68. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/application/__init__.py +0 -0
  69. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/application/base_worker.py +0 -0
  70. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/application/bus.py +0 -0
  71. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/application/di.py +0 -0
  72. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/application/registry.py +0 -0
  73. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/application/services/__init__.py +0 -0
  74. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/application/use_case.py +0 -0
  75. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/domain/__init__.py +0 -0
  76. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/domain/base.py +0 -0
  77. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/domain/models.py +0 -0
  78. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/ports/__init__.py +0 -0
  79. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/ports/cache.py +0 -0
  80. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/ports/event_bus.py +0 -0
  81. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/ports/files.py +0 -0
  82. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/ports/outbox.py +0 -0
  83. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/ports/repository.py +0 -0
  84. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/ports/translation.py +0 -0
  85. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/core/ports/uow.py +0 -0
  86. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/__init__.py +0 -0
  87. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/__init__.py +0 -0
  88. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/additional_filed/__init__.py +0 -0
  89. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/additional_filed/assembler.py +0 -0
  90. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/additional_filed/default_field_rules/__init__.py +0 -0
  91. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/additional_filed/default_field_rules/image_field_rules.py +0 -0
  92. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/additional_filed/field_registry.py +0 -0
  93. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/additional_filed/resolver.py +0 -0
  94. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/dependencies/client_credential.py +0 -0
  95. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
  96. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/dependencies/uow.py +0 -0
  97. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/exception_handlers.py +0 -0
  98. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/exceptions.py +0 -0
  99. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
  100. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
  101. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
  102. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/pagination/__init__.py +0 -0
  103. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -0
  104. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/routers.py +0 -0
  105. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/schema/__init__.py +0 -0
  106. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/schema/adaptive_schema.py +0 -0
  107. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/schema/base_response_model.py +0 -0
  108. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/api/schemas.py +0 -0
  109. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/cli/__init__.py +0 -0
  110. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/cli/crypto.py +0 -0
  111. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/cli/init_db.py +0 -0
  112. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/cli/main.py +0 -0
  113. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/cli/ticket.py +0 -0
  114. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/jobs/__init__.py +0 -0
  115. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/jobs/dispatch_outbox.py +0 -0
  116. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
  117. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/types.py +0 -0
  118. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/__init__.py +0 -0
  119. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/cache.py +0 -0
  120. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/cache_keys.py +0 -0
  121. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/cache_registry.py +0 -0
  122. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/context.py +0 -0
  123. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/crypto.py +0 -0
  124. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/flatten_dict.py +0 -0
  125. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/flatten_sqlalchemy_result.py +0 -0
  126. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/http.py +0 -0
  127. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/normalize_mobile.py +0 -0
  128. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/read_files.py +0 -0
  129. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/redactor.py +0 -0
  130. {nlbone-0.9.6 → nlbone-0.11.0}/src/nlbone/utils/time.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.9.6
3
+ Version: 0.11.0
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "nlbone"
7
- version = "0.9.6"
7
+ version = "0.11.0"
8
8
  description = "Backbone package for interfaces and infrastructure in Python projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -0,0 +1,3 @@
1
+ from .async_auth_service import AsyncAuthService, get_async_auth_service
2
+ from .auth_service import AuthService, get_auth_service
3
+ from .keycloak import KeycloakAuthService
@@ -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
- self,
80
- token_provider: ClientTokenProvider,
81
- base_url: Optional[str] = None,
82
- timeout_seconds: Optional[float] = None,
83
- client: httpx.Client | None = None,
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.token_provider import ClientTokenProvider
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: ClientTokenProvider | None = None,
17
+ token_provider: Optional[AsyncClientTokenProvider] = None,
18
18
  base_url: Optional[str] = None,
19
19
  timeout_seconds: Optional[float] = None,
20
- client: httpx.AsyncClient | None = None,
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, timeout=self._timeout, follow_redirects=True
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("", files=files, data=data, headers=auth_headers(tok))
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("", params=q, headers=auth_headers(tok))
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(f"/{file_id}", headers=auth_headers(tok))
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(f"/{file_id}/download", headers=auth_headers(tok), stream=True)
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(f"/{file_id}", headers=auth_headers(tok))
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"))
@@ -9,8 +9,8 @@ from nlbone.config.settings import get_settings
9
9
  class CreateTicketIn(BaseModel):
10
10
  assignee_id: str | None = None
11
11
  category_id: int
12
- channel: str
13
- direction: str
12
+ channel: str | None = None
13
+ direction: str | None = None
14
14
  entity_id: str
15
15
  entity_type: str
16
16
  message: str
@@ -19,6 +19,9 @@ class CreateTicketIn(BaseModel):
19
19
  status: str
20
20
  title: str
21
21
  user_id: int
22
+ order_item_id: str
23
+ product_variant_id: str
24
+ type: str
22
25
 
23
26
 
24
27
  class TicketingClient:
@@ -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/files")
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=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]: ...
@@ -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