nlbone 0.5.0__tar.gz → 0.6.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 (93) hide show
  1. {nlbone-0.5.0 → nlbone-0.6.0}/PKG-INFO +1 -1
  2. {nlbone-0.5.0 → nlbone-0.6.0}/pyproject.toml +1 -1
  3. nlbone-0.6.0/src/nlbone/adapters/http_clients/__init__.py +2 -0
  4. nlbone-0.6.0/src/nlbone/adapters/http_clients/pricing/__init__.py +1 -0
  5. nlbone-0.6.0/src/nlbone/adapters/http_clients/pricing/pricing_service.py +96 -0
  6. nlbone-0.6.0/src/nlbone/adapters/http_clients/uploadchi/__init__.py +2 -0
  7. {nlbone-0.5.0/src/nlbone/adapters/http_clients → nlbone-0.6.0/src/nlbone/adapters/http_clients/uploadchi}/uploadchi.py +10 -34
  8. {nlbone-0.5.0/src/nlbone/adapters/http_clients → nlbone-0.6.0/src/nlbone/adapters/http_clients/uploadchi}/uploadchi_async.py +11 -10
  9. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/config/settings.py +20 -12
  10. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/container.py +3 -1
  11. nlbone-0.6.0/src/nlbone/utils/http.py +29 -0
  12. nlbone-0.5.0/src/nlbone/adapters/http_clients/email_gateway.py +0 -0
  13. nlbone-0.5.0/src/nlbone/utils/__init__.py +0 -0
  14. {nlbone-0.5.0 → nlbone-0.6.0}/.gitignore +0 -0
  15. {nlbone-0.5.0 → nlbone-0.6.0}/LICENSE +0 -0
  16. {nlbone-0.5.0 → nlbone-0.6.0}/README.md +0 -0
  17. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/__init__.py +0 -0
  18. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/__init__.py +0 -0
  19. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/auth/__init__.py +0 -0
  20. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/auth/keycloak.py +0 -0
  21. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/auth/token_provider.py +0 -0
  22. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/cache/__init__.py +0 -0
  23. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/cache/async_redis.py +0 -0
  24. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/cache/memory.py +0 -0
  25. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/cache/pubsub_listener.py +0 -0
  26. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/cache/redis.py +0 -0
  27. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/__init__.py +0 -0
  28. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/postgres/__init__.py +0 -0
  29. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/postgres/audit.py +0 -0
  30. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/postgres/base.py +0 -0
  31. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/postgres/engine.py +0 -0
  32. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/postgres/query_builder.py +0 -0
  33. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/postgres/repository.py +0 -0
  34. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/postgres/schema.py +0 -0
  35. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/postgres/uow.py +0 -0
  36. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/redis/__init__.py +0 -0
  37. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/db/redis/client.py +0 -0
  38. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/messaging/__init__.py +0 -0
  39. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/messaging/event_bus.py +0 -0
  40. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/messaging/redis.py +0 -0
  41. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/percolation/__init__.py +0 -0
  42. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/adapters/percolation/connection.py +0 -0
  43. {nlbone-0.5.0/src/nlbone/adapters/http_clients → nlbone-0.6.0/src/nlbone/config}/__init__.py +0 -0
  44. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/config/logging.py +0 -0
  45. {nlbone-0.5.0/src/nlbone/config → nlbone-0.6.0/src/nlbone/core}/__init__.py +0 -0
  46. {nlbone-0.5.0/src/nlbone/core → nlbone-0.6.0/src/nlbone/core/application}/__init__.py +0 -0
  47. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/application/base_worker.py +0 -0
  48. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/application/events.py +0 -0
  49. {nlbone-0.5.0/src/nlbone/core/application → nlbone-0.6.0/src/nlbone/core/application/services}/__init__.py +0 -0
  50. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/application/services.py +0 -0
  51. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/application/use_case.py +0 -0
  52. {nlbone-0.5.0/src/nlbone/core/application/services → nlbone-0.6.0/src/nlbone/core/domain}/__init__.py +0 -0
  53. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/domain/base.py +0 -0
  54. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/domain/events.py +0 -0
  55. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/domain/models.py +0 -0
  56. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/ports/__init__.py +0 -0
  57. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/ports/auth.py +0 -0
  58. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/ports/cache.py +0 -0
  59. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/ports/event_bus.py +0 -0
  60. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/ports/files.py +0 -0
  61. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/ports/messaging.py +0 -0
  62. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/ports/repo.py +0 -0
  63. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/core/ports/uow.py +0 -0
  64. {nlbone-0.5.0/src/nlbone/core/domain → nlbone-0.6.0/src/nlbone/interfaces}/__init__.py +0 -0
  65. {nlbone-0.5.0/src/nlbone/interfaces → nlbone-0.6.0/src/nlbone/interfaces/api}/__init__.py +0 -0
  66. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/dependencies/__init__.py +0 -0
  67. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/dependencies/async_auth.py +0 -0
  68. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/dependencies/auth.py +0 -0
  69. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/dependencies/db.py +0 -0
  70. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/dependencies/uow.py +0 -0
  71. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/exception_handlers.py +0 -0
  72. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/exceptions.py +0 -0
  73. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/middleware/__init__.py +0 -0
  74. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/middleware/access_log.py +0 -0
  75. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/middleware/add_request_context.py +0 -0
  76. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/middleware/authentication.py +0 -0
  77. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/pagination/__init__.py +0 -0
  78. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/pagination/offset_base.py +0 -0
  79. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/routers.py +0 -0
  80. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/api/schemas.py +0 -0
  81. {nlbone-0.5.0/src/nlbone/interfaces/api → nlbone-0.6.0/src/nlbone/interfaces/cli}/__init__.py +0 -0
  82. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/cli/init_db.py +0 -0
  83. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/cli/main.py +0 -0
  84. {nlbone-0.5.0/src/nlbone/interfaces/cli → nlbone-0.6.0/src/nlbone/interfaces/jobs}/__init__.py +0 -0
  85. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/interfaces/jobs/sync_tokens.py +0 -0
  86. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/types.py +0 -0
  87. {nlbone-0.5.0/src/nlbone/interfaces/jobs → nlbone-0.6.0/src/nlbone/utils}/__init__.py +0 -0
  88. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/utils/cache.py +0 -0
  89. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/utils/cache_keys.py +0 -0
  90. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/utils/cache_registry.py +0 -0
  91. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/utils/context.py +0 -0
  92. {nlbone-0.5.0 → nlbone-0.6.0}/src/nlbone/utils/redactor.py +0 -0
  93. {nlbone-0.5.0 → nlbone-0.6.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.5.0
3
+ Version: 0.6.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.5.0"
7
+ version = "0.6.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,2 @@
1
+ from .uploadchi import UploadchiClient, UploadchiAsyncClient, UploadchiError
2
+ from .pricing import PricingService, CalculatePriceIn, CalculatePriceOut
@@ -0,0 +1 @@
1
+ from .pricing_service import PricingService, CalculatePriceIn, CalculatePriceOut
@@ -0,0 +1,96 @@
1
+ from enum import Enum
2
+ from typing import Optional, Literal, List
3
+
4
+ import httpx
5
+ import requests
6
+ from pydantic import BaseModel, Field, NonNegativeInt, RootModel
7
+
8
+ from nlbone.adapters.auth.token_provider import ClientTokenProvider
9
+ from nlbone.config.settings import get_settings
10
+ from nlbone.utils.http import normalize_https_base, auth_headers
11
+
12
+
13
+ class PricingError(Exception):
14
+ pass
15
+
16
+
17
+ class CalculatePriceIn(BaseModel):
18
+ params: dict[str, str]
19
+ product_id: NonNegativeInt | None = None
20
+ product_title: str | None = None
21
+
22
+
23
+ class DiscountType(str, Enum):
24
+ percent = "percent"
25
+ amount = "amount"
26
+
27
+
28
+ class Product(BaseModel):
29
+ id: Optional[int] = Field(None, description="Nullable product id")
30
+ service_product_id: NonNegativeInt
31
+ title: Optional[str] = None
32
+
33
+
34
+ class Pricing(BaseModel):
35
+ source: Literal["formula", "static"]
36
+ price: float
37
+ discount: Optional[float] = None
38
+ discount_type: Optional[DiscountType] = None
39
+ params: dict
40
+
41
+
42
+ class Formula(BaseModel):
43
+ id: int
44
+ title: str
45
+ key: str
46
+ status: str
47
+ description: str
48
+
49
+
50
+ class PricingRule(BaseModel):
51
+ product: Product
52
+ segment_name: str | None
53
+ formula: Optional[Formula] = None
54
+ specificity: int
55
+ matched_fields: list
56
+ pricing: Pricing
57
+
58
+
59
+ class CalculatePriceOut(RootModel[List[PricingRule]]):
60
+ pass
61
+
62
+
63
+ class PricingService:
64
+ def __init__(
65
+ self,
66
+ token_provider: ClientTokenProvider,
67
+ base_url: Optional[str] = None,
68
+ timeout_seconds: Optional[float] = None,
69
+ client: httpx.Client | None = None,
70
+ ) -> None:
71
+ s = get_settings()
72
+ self._base_url = normalize_https_base(base_url or str(s.PRICING_SERVICE_URL), enforce_https=False)
73
+ self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
74
+ self._client = client or requests.session()
75
+ self._token_provider = token_provider
76
+
77
+ def calculate(self, items: list[CalculatePriceIn]) -> CalculatePriceOut:
78
+ body = {
79
+ "items": [i.model_dump() for i in items]
80
+ }
81
+
82
+ r = self._client.post(
83
+ f"{self._base_url}/priced",
84
+ headers=auth_headers(self._token_provider.get_access_token()),
85
+ json=body,
86
+ timeout=self._timeout,
87
+ verify=False
88
+ )
89
+
90
+ if r.status_code not in (200, 204):
91
+ raise PricingError(r.status_code, r.text)
92
+
93
+ if r.status_code == 204 or not r.content:
94
+ return CalculatePriceOut.model_validate(root=[])
95
+
96
+ return CalculatePriceOut.model_validate(r.json())
@@ -0,0 +1,2 @@
1
+ from .uploadchi import UploadchiClient, UploadchiError
2
+ from .uploadchi_async import UploadchiAsyncClient
@@ -1,8 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
4
3
  from typing import Any, Optional
5
- from urllib.parse import urlparse, urlunparse
6
4
 
7
5
  import httpx
8
6
  import requests
@@ -10,6 +8,7 @@ import requests
10
8
  from nlbone.adapters.auth.token_provider import ClientTokenProvider
11
9
  from nlbone.config.settings import get_settings
12
10
  from nlbone.core.ports.files import FileServicePort
11
+ from nlbone.utils.http import auth_headers, build_list_query, normalize_https_base
13
12
 
14
13
 
15
14
  class UploadchiError(RuntimeError):
@@ -26,21 +25,6 @@ def _resolve_token(explicit: str | None) -> str | None:
26
25
  return s.UPLOADCHI_TOKEN.get_secret_value() if s.UPLOADCHI_TOKEN else None
27
26
 
28
27
 
29
- def _auth_headers(token: str | None) -> dict[str, str]:
30
- return {"Authorization": f"Bearer {token}"} if token else {}
31
-
32
-
33
- def _build_list_query(
34
- limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
35
- ) -> dict[str, Any]:
36
- q: dict[str, Any] = {"limit": limit, "offset": offset}
37
- if filters:
38
- q["filters"] = json.dumps(filters)
39
- if sort:
40
- q["sort"] = ",".join([f"{f}:{o}" for f, o in sort])
41
- return q
42
-
43
-
44
28
  def _filename_from_cd(cd: str | None, fallback: str) -> str:
45
29
  if not cd:
46
30
  return fallback
@@ -49,14 +33,6 @@ def _filename_from_cd(cd: str | None, fallback: str) -> str:
49
33
  return fallback
50
34
 
51
35
 
52
- def _normalize_https_base(url: str) -> str:
53
- p = urlparse(url.strip())
54
- p = p._replace(scheme="https") # enforce https
55
- if p.path.endswith("/"):
56
- p = p._replace(path=p.path.rstrip("/"))
57
- return str(urlunparse(p))
58
-
59
-
60
36
  class UploadchiClient(FileServicePort):
61
37
  def __init__(
62
38
  self,
@@ -66,7 +42,7 @@ class UploadchiClient(FileServicePort):
66
42
  client: httpx.Client | None = None,
67
43
  ) -> None:
68
44
  s = get_settings()
69
- self._base_url = _normalize_https_base(base_url or str(s.UPLOADCHI_BASE_URL))
45
+ self._base_url = normalize_https_base(base_url or str(s.UPLOADCHI_BASE_URL))
70
46
  self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
71
47
  self._client = client or requests.session()
72
48
  self._token_provider = token_provider
@@ -80,7 +56,7 @@ class UploadchiClient(FileServicePort):
80
56
  tok = _resolve_token(token)
81
57
  files = {"file": (filename, file_bytes)}
82
58
  data = (params or {}).copy()
83
- r = self._client.post(self._base_url, files=files, data=data, headers=_auth_headers(tok))
59
+ r = self._client.post(self._base_url, files=files, data=data, headers=auth_headers(tok))
84
60
  if r.status_code >= 400:
85
61
  raise UploadchiError(r.status_code, r.text)
86
62
  return r.json()
@@ -91,7 +67,7 @@ class UploadchiClient(FileServicePort):
91
67
  tok = _resolve_token(token)
92
68
  r = self._client.post(
93
69
  f"{self._base_url}/{file_id}/commit",
94
- headers=_auth_headers(tok or self._token_provider.get_access_token()),
70
+ headers=auth_headers(tok or self._token_provider.get_access_token()),
95
71
  )
96
72
  if r.status_code not in (204, 200):
97
73
  raise UploadchiError(r.status_code, r.text)
@@ -102,7 +78,7 @@ class UploadchiClient(FileServicePort):
102
78
  tok = _resolve_token(token)
103
79
  r = self._client.post(
104
80
  f"{self._base_url}/{file_id}/rollback",
105
- headers=_auth_headers(tok or self._token_provider.get_access_token()),
81
+ headers=auth_headers(tok or self._token_provider.get_access_token()),
106
82
  )
107
83
  if r.status_code not in (204, 200):
108
84
  raise UploadchiError(r.status_code, r.text)
@@ -116,22 +92,22 @@ class UploadchiClient(FileServicePort):
116
92
  token: str | None = None,
117
93
  ) -> dict:
118
94
  tok = _resolve_token(token)
119
- q = _build_list_query(limit, offset, filters, sort)
120
- r = self._client.get(self._base_url, params=q, headers=_auth_headers(tok))
95
+ q = build_list_query(limit, offset, filters, sort)
96
+ r = self._client.get(self._base_url, params=q, headers=auth_headers(tok))
121
97
  if r.status_code >= 400:
122
98
  raise UploadchiError(r.status_code, r.text)
123
99
  return r.json()
124
100
 
125
101
  def get_file(self, file_id: str, token: str | None = None) -> dict:
126
102
  tok = _resolve_token(token)
127
- r = self._client.get(f"{self._base_url}/{file_id}", headers=_auth_headers(tok))
103
+ r = self._client.get(f"{self._base_url}/{file_id}", headers=auth_headers(tok))
128
104
  if r.status_code >= 400:
129
105
  raise UploadchiError(r.status_code, r.text)
130
106
  return r.json()
131
107
 
132
108
  def download_file(self, file_id: str, token: str | None = None) -> tuple[bytes, str, str]:
133
109
  tok = _resolve_token(token)
134
- r = self._client.get(f"{self._base_url}/{file_id}/download", headers=_auth_headers(tok))
110
+ r = self._client.get(f"{self._base_url}/{file_id}/download", headers=auth_headers(tok))
135
111
  if r.status_code >= 400:
136
112
  raise UploadchiError(r.status_code, r.text)
137
113
  filename = _filename_from_cd(r.headers.get("content-disposition"), fallback=f"file-{file_id}")
@@ -140,6 +116,6 @@ class UploadchiClient(FileServicePort):
140
116
 
141
117
  def delete_file(self, file_id: str, token: str | None = None) -> None:
142
118
  tok = _resolve_token(token)
143
- r = self._client.delete(f"{self._base_url}/{file_id}", headers=_auth_headers(tok))
119
+ r = self._client.delete(f"{self._base_url}/{file_id}", headers=auth_headers(tok))
144
120
  if r.status_code not in (204, 200):
145
121
  raise UploadchiError(r.status_code, r.text)
@@ -7,8 +7,9 @@ import httpx
7
7
  from nlbone.config.settings import get_settings
8
8
  from nlbone.core.ports.files import AsyncFileServicePort
9
9
 
10
- from .uploadchi import UploadchiError, _auth_headers, _build_list_query, _filename_from_cd, _resolve_token
11
- from ..auth.token_provider import ClientTokenProvider
10
+ from nlbone.adapters.http_clients.uploadchi.uploadchi import UploadchiError, _filename_from_cd, _resolve_token
11
+ from nlbone.adapters.auth.token_provider import ClientTokenProvider
12
+ from nlbone.utils.http import auth_headers, build_list_query
12
13
 
13
14
 
14
15
  class UploadchiAsyncClient(AsyncFileServicePort):
@@ -36,7 +37,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
36
37
  tok = _resolve_token(token)
37
38
  files = {"file": (filename, file_bytes)}
38
39
  data = (params or {}).copy()
39
- r = await self._client.post("", files=files, data=data, headers=_auth_headers(tok))
40
+ r = await self._client.post("", files=files, data=data, headers=auth_headers(tok))
40
41
  if r.status_code >= 400:
41
42
  raise UploadchiError(r.status_code, await r.aread())
42
43
  return r.json()
@@ -46,7 +47,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
46
47
  raise UploadchiError(detail="token_provider is not provided", status=400)
47
48
  tok = _resolve_token(token)
48
49
  r = await self._client.post(
49
- f"/{file_id}/commit", headers=_auth_headers(tok or self._token_provider.get_access_token())
50
+ f"/{file_id}/commit", headers=auth_headers(tok or self._token_provider.get_access_token())
50
51
  )
51
52
  if r.status_code not in (204, 200):
52
53
  raise UploadchiError(r.status_code, await r.aread())
@@ -56,7 +57,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
56
57
  raise UploadchiError(detail="token_provider is not provided", status=400)
57
58
  tok = _resolve_token(token)
58
59
  r = await self._client.post(
59
- f"/{file_id}/rollback", headers=_auth_headers(tok or self._token_provider.get_access_token())
60
+ f"/{file_id}/rollback", headers=auth_headers(tok or self._token_provider.get_access_token())
60
61
  )
61
62
  if r.status_code not in (204, 200):
62
63
  raise UploadchiError(r.status_code, await r.aread())
@@ -70,22 +71,22 @@ class UploadchiAsyncClient(AsyncFileServicePort):
70
71
  token: str | None = None,
71
72
  ) -> dict:
72
73
  tok = _resolve_token(token)
73
- q = _build_list_query(limit, offset, filters, sort)
74
- r = await self._client.get("", params=q, headers=_auth_headers(tok))
74
+ q = build_list_query(limit, offset, filters, sort)
75
+ r = await self._client.get("", params=q, headers=auth_headers(tok))
75
76
  if r.status_code >= 400:
76
77
  raise UploadchiError(r.status_code, await r.aread())
77
78
  return r.json()
78
79
 
79
80
  async def get_file(self, file_id: str, token: str | None = None) -> dict:
80
81
  tok = _resolve_token(token)
81
- r = await self._client.get(f"/{file_id}", headers=_auth_headers(tok))
82
+ r = await self._client.get(f"/{file_id}", headers=auth_headers(tok))
82
83
  if r.status_code >= 400:
83
84
  raise UploadchiError(r.status_code, await r.aread())
84
85
  return r.json()
85
86
 
86
87
  async def download_file(self, file_id: str, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]:
87
88
  tok = _resolve_token(token)
88
- r = await self._client.get(f"/{file_id}/download", headers=_auth_headers(tok), stream=True)
89
+ r = await self._client.get(f"/{file_id}/download", headers=auth_headers(tok), stream=True)
89
90
  if r.status_code >= 400:
90
91
  body = await r.aread()
91
92
  raise UploadchiError(r.status_code, body.decode(errors="ignore"))
@@ -103,7 +104,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
103
104
 
104
105
  async def delete_file(self, file_id: str, token: str | None = None) -> None:
105
106
  tok = _resolve_token(token)
106
- r = await self._client.delete(f"/{file_id}", headers=_auth_headers(tok))
107
+ r = await self._client.delete(f"/{file_id}", headers=auth_headers(tok))
107
108
  if r.status_code not in (204, 200):
108
109
  body = await r.aread()
109
110
  raise UploadchiError(r.status_code, body.decode(errors="ignore"))
@@ -8,19 +8,22 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
8
8
 
9
9
 
10
10
  def _guess_env_file() -> str | None:
11
- explicit = os.getenv("NLBONE_ENV_FILE")
12
- if explicit:
13
- return explicit
11
+ try:
12
+ explicit = os.getenv("NLBONE_ENV_FILE")
13
+ if explicit:
14
+ return explicit
14
15
 
15
- cwd_env = Path.cwd() / ".env"
16
- if cwd_env.exists():
17
- return str(cwd_env)
16
+ cwd_env = Path.cwd() / ".env"
17
+ if cwd_env.exists():
18
+ return str(cwd_env)
18
19
 
19
- for i in range(0, 8):
20
- p = Path.cwd().resolve().parents[i]
21
- f = p / ".env"
22
- if f.exists():
23
- return str(f)
20
+ for i in range(0, 8):
21
+ p = Path.cwd().resolve().parents[i]
22
+ f = p / ".env"
23
+ if f.exists():
24
+ return str(f)
25
+ except Exception as e:
26
+ raise Exception("Failed to guess env file path!") from e
24
27
 
25
28
 
26
29
  def is_production_env() -> bool:
@@ -82,10 +85,15 @@ class Settings(BaseSettings):
82
85
  # ---------------------------
83
86
  # PERCOLATE
84
87
  # ---------------------------
85
- ELASTIC_PERCOLATE_URL: str = Field(default="http://localhost:9200")
88
+ ELASTIC_PERCOLATE_URL: str = Field(default="http://localhost:9200")
86
89
  ELASTIC_PERCOLATE_USER: str = Field(default="")
87
90
  ELASTIC_PERCOLATE_PASS: SecretStr = Field(default="")
88
91
 
92
+ # ---------------------------
93
+ # Pricing
94
+ # ---------------------------
95
+ PRICING_SERVICE_URL: AnyHttpUrl = Field(default="https://pricing.numberland.ir/v1")
96
+
89
97
  model_config = SettingsConfigDict(
90
98
  env_prefix="",
91
99
  env_file=None,
@@ -11,8 +11,9 @@ from nlbone.adapters.cache.memory import InMemoryCache
11
11
  from nlbone.adapters.cache.redis import RedisCache
12
12
  from nlbone.adapters.db.postgres import AsyncSqlAlchemyUnitOfWork, SqlAlchemyUnitOfWork
13
13
  from nlbone.adapters.db.postgres.engine import get_async_session_factory, get_sync_session_factory
14
+ from nlbone.adapters.http_clients import PricingService
14
15
  from nlbone.adapters.http_clients.uploadchi import UploadchiClient
15
- from nlbone.adapters.http_clients.uploadchi_async import UploadchiAsyncClient
16
+ from nlbone.adapters.http_clients.uploadchi.uploadchi_async import UploadchiAsyncClient
16
17
  from nlbone.adapters.messaging import InMemoryEventBus
17
18
  from nlbone.core.ports import EventBusPort
18
19
  from nlbone.core.ports.cache import CachePort, AsyncCachePort
@@ -39,6 +40,7 @@ class Container(containers.DeclarativeContainer):
39
40
  token_provider=token_provider)
40
41
  afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(UploadchiAsyncClient,
41
42
  token_provider=token_provider)
43
+ pricing_service: providers.Singleton[PricingService] = providers.Singleton(PricingService, token_provider=token_provider)
42
44
 
43
45
  cache: providers.Singleton[CachePort] = providers.Selector(
44
46
  config.CACHE_BACKEND,
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+ from urllib.parse import urlparse, urlunparse
6
+
7
+
8
+ def auth_headers(token: str | None) -> dict[str, str]:
9
+ return {"Authorization": f"Bearer {token}"} if token else {}
10
+
11
+
12
+ def build_list_query(
13
+ limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
14
+ ) -> dict[str, Any]:
15
+ q: dict[str, Any] = {"limit": limit, "offset": offset}
16
+ if filters:
17
+ q["filters"] = json.dumps(filters)
18
+ if sort:
19
+ q["sort"] = ",".join([f"{f}:{o}" for f, o in sort])
20
+ return q
21
+
22
+
23
+ def normalize_https_base(url: str, enforce_https: bool = True) -> str:
24
+ p = urlparse(url.strip())
25
+ if enforce_https:
26
+ p = p._replace(scheme="https") # enforce https
27
+ if p.path.endswith("/"):
28
+ p = p._replace(path=p.path.rstrip("/"))
29
+ return str(urlunparse(p))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes