nlbone 0.1.31__py3-none-any.whl → 0.1.32__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.
@@ -1,177 +1,81 @@
1
1
  from __future__ import annotations
2
-
3
2
  import json
4
- from typing import Any, AsyncIterator, Optional
5
-
3
+ from typing import Any, Optional
6
4
  import httpx
7
5
 
8
6
  from nlbone.core.ports.files import FileServicePort
9
7
  from nlbone.config.settings import get_settings
10
8
 
11
-
12
- class FileServiceException(RuntimeError):
9
+ class UploadchiError(RuntimeError):
13
10
  def __init__(self, status: int, detail: Any | None = None):
14
11
  super().__init__(f"Uploadchi HTTP {status}: {detail}")
15
12
  self.status = status
16
13
  self.detail = detail
17
14
 
18
-
19
15
  def _auth_headers(token: str | None) -> dict[str, str]:
20
16
  return {"Authorization": f"Bearer {token}"} if token else {}
21
17
 
22
-
23
- def _build_list_query(
24
- limit: int,
25
- offset: int,
26
- filters: dict[str, Any] | None,
27
- sort: list[tuple[str, str]] | None,
28
- ) -> dict[str, Any]:
18
+ def _build_list_query(limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None) -> dict[str, Any]:
29
19
  q: dict[str, Any] = {"limit": limit, "offset": offset}
30
20
  if filters:
31
- # سرور شما `filters` را به صورت string می‌گیرد؛
32
- # اگر سمت سرور JSON هم قبول می‌کند، این بهتر است:
33
21
  q["filters"] = json.dumps(filters)
34
22
  if sort:
35
- # "created_at:desc,id:asc"
36
23
  q["sort"] = ",".join([f"{f}:{o}" for f, o in sort])
37
24
  return q
38
25
 
26
+ def _filename_from_cd(cd: str | None, fallback: str) -> str:
27
+ if not cd:
28
+ return fallback
29
+ if "filename=" in cd:
30
+ return cd.split("filename=", 1)[1].strip("\"'")
31
+ return fallback
39
32
 
40
33
  class UploadchiClient(FileServicePort):
41
- """
42
- httpx-based client for the Uploadchi microservice.
43
-
44
- Base URL نمونه: http://uploadchi.internal/api/v1/files
45
- """
46
-
47
- def __init__(
48
- self,
49
- base_url: Optional[str] = None,
50
- timeout_seconds: Optional[float] = None,
51
- client: httpx.AsyncClient | None = None,
52
- ) -> None:
34
+ """Sync client using httpx.Client"""
35
+ def __init__(self, base_url: Optional[str] = None, timeout_seconds: Optional[float] = None, client: httpx.Client | None = None) -> None:
53
36
  s = get_settings()
54
37
  self._base_url = base_url or str(s.UPLOADCHI_BASE_URL)
55
38
  self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
56
- # اگر کلاینت تزریق نشد، خودمان می‌سازیم
57
- self._client = client or httpx.AsyncClient(
58
- base_url=self._base_url,
59
- timeout=self._timeout,
60
- follow_redirects=True,
61
- )
62
-
63
- async def aclose(self) -> None:
64
- await self._client.aclose()
39
+ self._client = client or httpx.Client(base_url=self._base_url, timeout=self._timeout, follow_redirects=True)
65
40
 
66
- # ---------- Endpoints ----------
41
+ def close(self) -> None:
42
+ self._client.close()
67
43
 
68
- async def upload_file(
69
- self,
70
- file_bytes: bytes,
71
- filename: str,
72
- params: dict[str, Any] | None = None,
73
- token: str | None = None,
74
- ) -> dict:
75
- """
76
- POST "" → returns FileOut (dict)
77
- fields:
78
- - file: Upload (multipart)
79
- - other params (e.g., bucket, folder, content_type, ...)
80
- """
44
+ def upload_file(self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None) -> dict:
81
45
  files = {"file": (filename, file_bytes)}
82
46
  data = (params or {}).copy()
83
- r = await self._client.post(
84
- url="",
85
- files=files,
86
- data=data,
87
- headers=_auth_headers(token),
88
- )
47
+ r = self._client.post("", files=files, data=data, headers=_auth_headers(token))
89
48
  if r.status_code >= 400:
90
- raise FileServiceException(r.status_code, r.text)
49
+ raise UploadchiError(r.status_code, r.text)
91
50
  return r.json()
92
51
 
93
- async def commit_file(
94
- self,
95
- file_id: int,
96
- client_id: str,
97
- token: str | None = None,
98
- ) -> None:
99
- """
100
- POST "/{file_id}/commit" 204
101
- """
102
- r = await self._client.post(
103
- f"/{file_id}/commit",
104
- headers=_auth_headers(token),
105
- params={"client_id": client_id} if client_id else None,
106
- )
52
+ def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None:
53
+ r = self._client.post(f"/{file_id}/commit", headers=_auth_headers(token), params={"client_id": client_id} if client_id else None)
107
54
  if r.status_code not in (204, 200):
108
- raise FileServiceException(r.status_code, r.text)
55
+ raise UploadchiError(r.status_code, r.text)
109
56
 
110
- async def list_files(
111
- self,
112
- limit: int = 10,
113
- offset: int = 0,
114
- filters: dict[str, Any] | None = None,
115
- sort: list[tuple[str, str]] | None = None,
116
- token: str | None = None,
117
- ) -> dict:
118
- """
119
- GET "" → returns PaginateResponse-like dict
120
- { "data": [...], "total_count": int | null, "total_page": int | null }
121
- """
57
+ def list_files(self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None, sort: list[tuple[str, str]] | None = None, token: str | None = None) -> dict:
122
58
  q = _build_list_query(limit, offset, filters, sort)
123
- r = await self._client.get("", params=q, headers=_auth_headers(token))
59
+ r = self._client.get("", params=q, headers=_auth_headers(token))
124
60
  if r.status_code >= 400:
125
- raise FileServiceException(r.status_code, r.text)
61
+ raise UploadchiError(r.status_code, r.text)
126
62
  return r.json()
127
63
 
128
- async def get_file(
129
- self,
130
- file_id: int,
131
- token: str | None = None,
132
- ) -> dict:
133
- """
134
- GET "/{file_id}" → FileOut dict
135
- """
136
- r = await self._client.get(f"/{file_id}", headers=_auth_headers(token))
64
+ def get_file(self, file_id: int, token: str | None = None) -> dict:
65
+ r = self._client.get(f"/{file_id}", headers=_auth_headers(token))
137
66
  if r.status_code >= 400:
138
- raise FileServiceException(r.status_code, r.text)
67
+ raise UploadchiError(r.status_code, r.text)
139
68
  return r.json()
140
69
 
141
- async def download_file(
142
- self,
143
- file_id: int,
144
- token: str | None = None,
145
- ) -> tuple[AsyncIterator[bytes], str, str]:
146
- """
147
- GET "/{file_id}/download" → stream + headers (filename, content-type)
148
- """
149
- r = await self._client.get(f"/{file_id}/download", headers=_auth_headers(token))
70
+ def download_file(self, file_id: int, token: str | None = None) -> tuple[bytes, str, str]:
71
+ r = self._client.get(f"/{file_id}/download", headers=_auth_headers(token))
150
72
  if r.status_code >= 400:
151
- text = await r.aread()
152
- raise FileServiceException(r.status_code, text.decode(errors="ignore"))
153
-
154
- disp = r.headers.get("content-disposition", "")
155
- filename = (
156
- disp.split("filename=", 1)[1].strip("\"'") if "filename=" in disp else f"file-{file_id}"
157
- )
73
+ raise UploadchiError(r.status_code, r.text)
74
+ filename = _filename_from_cd(r.headers.get("content-disposition"), fallback=f"file-{file_id}")
158
75
  media_type = r.headers.get("content-type", "application/octet-stream")
76
+ return r.content, filename, media_type
159
77
 
160
- async def _aiter() -> AsyncIterator[bytes]:
161
- async for chunk in r.aiter_bytes():
162
- yield chunk
163
- await r.aclose()
164
-
165
- return _aiter(), filename, media_type
166
-
167
- async def delete_file(
168
- self,
169
- file_id: int,
170
- token: str | None = None,
171
- ) -> None:
172
- """
173
- DELETE "/{file_id}" → 204
174
- """
175
- r = await self._client.delete(f"/{file_id}", headers=_auth_headers(token))
78
+ def delete_file(self, file_id: int, token: str | None = None) -> None:
79
+ r = self._client.delete(f"/{file_id}", headers=_auth_headers(token))
176
80
  if r.status_code not in (204, 200):
177
- raise FileServiceException(r.status_code, r.text)
81
+ raise UploadchiError(r.status_code, r.text)
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from typing import Any, Optional, AsyncIterator
4
+ import httpx
5
+
6
+ from nlbone.core.ports.files import AsyncFileServicePort
7
+ from nlbone.config.settings import get_settings
8
+ from .uploadchi import UploadchiError, _auth_headers, _build_list_query, _filename_from_cd
9
+
10
+ class UploadchiAsyncClient(AsyncFileServicePort):
11
+ """Async client using httpx.AsyncClient; method names prefixed with a_"""
12
+ def __init__(self, base_url: Optional[str] = None, timeout_seconds: Optional[float] = None, client: httpx.AsyncClient | None = None) -> None:
13
+ s = get_settings()
14
+ self._base_url = base_url or str(s.UPLOADCHI_BASE_URL)
15
+ self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
16
+ self._client = client or httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout, follow_redirects=True)
17
+
18
+ async def aclose(self) -> None:
19
+ await self._client.aclose()
20
+
21
+ async def a_upload_file(self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None) -> dict:
22
+ files = {"file": (filename, file_bytes)}
23
+ data = (params or {}).copy()
24
+ r = await self._client.post("", files=files, data=data, headers=_auth_headers(token))
25
+ if r.status_code >= 400:
26
+ raise UploadchiError(r.status_code, r.text)
27
+ return r.json()
28
+
29
+ async def a_commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None:
30
+ r = await self._client.post(f"/{file_id}/commit", headers=_auth_headers(token), params={"client_id": client_id} if client_id else None)
31
+ if r.status_code not in (204, 200):
32
+ raise UploadchiError(r.status_code, r.text)
33
+
34
+ async def a_list_files(self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None, sort: list[tuple[str, str]] | None = None, token: str | None = None) -> dict:
35
+ q = _build_list_query(limit, offset, filters, sort)
36
+ r = await self._client.get("", params=q, headers=_auth_headers(token))
37
+ if r.status_code >= 400:
38
+ raise UploadchiError(r.status_code, r.text)
39
+ return r.json()
40
+
41
+ async def a_get_file(self, file_id: int, token: str | None = None) -> dict:
42
+ r = await self._client.get(f"/{file_id}", headers=_auth_headers(token))
43
+ if r.status_code >= 400:
44
+ raise UploadchiError(r.status_code, r.text)
45
+ return r.json()
46
+
47
+ async def a_download_file(self, file_id: int, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]:
48
+ r = await self._client.get(f"/{file_id}/download", headers=_auth_headers(token), stream=True)
49
+ if r.status_code >= 400:
50
+ body = await r.aread()
51
+ raise UploadchiError(r.status_code, body.decode(errors="ignore"))
52
+ filename = _filename_from_cd(r.headers.get("content-disposition"), fallback=f"file-{file_id}")
53
+ media_type = r.headers.get("content-type", "application/octet-stream")
54
+
55
+ async def _aiter() -> AsyncIterator[bytes]:
56
+ try:
57
+ async for chunk in r.aiter_bytes():
58
+ yield chunk
59
+ finally:
60
+ await r.aclose()
61
+
62
+ return _aiter(), filename, media_type
63
+
64
+ async def a_delete_file(self, file_id: int, token: str | None = None) -> None:
65
+ r = await self._client.delete(f"/{file_id}", headers=_auth_headers(token))
66
+ if r.status_code not in (204, 200):
67
+ raise UploadchiError(r.status_code, r.text)
@@ -1,7 +1,8 @@
1
1
  from nlbone.adapters.http_clients.uploadchi import UploadchiClient
2
+ from nlbone.adapters.http_clients.uploadchi_async import UploadchiAsyncClient
2
3
  from nlbone.config.settings import Settings
3
4
  from nlbone.adapters.auth.keycloak import KeycloakAuthService
4
- from nlbone.core.ports.files import FileServicePort
5
+ from nlbone.core.ports.files import FileServicePort, AsyncFileServicePort
5
6
 
6
7
 
7
8
  class Container:
@@ -9,3 +10,4 @@ class Container:
9
10
  self.settings = settings or Settings()
10
11
  self.auth: KeycloakAuthService = KeycloakAuthService(self.settings)
11
12
  self.file_service: FileServicePort = UploadchiClient()
13
+ self.afiles_service: AsyncFileServicePort = UploadchiAsyncClient()
@@ -3,45 +3,20 @@ from typing import Protocol, runtime_checkable, AsyncIterator, Any
3
3
 
4
4
  @runtime_checkable
5
5
  class FileServicePort(Protocol):
6
- async def upload_file(
7
- self,
8
- file_bytes: bytes,
9
- filename: str,
10
- params: dict[str, Any] | None = None,
11
- token: str | None = None,
12
- ) -> dict: ...
6
+ # ---- Sync API ----
7
+ def upload_file(self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None) -> dict: ...
8
+ def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None: ...
9
+ def list_files(self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None, sort: list[tuple[str, str]] | None = None, token: str | None = None) -> dict: ...
10
+ def get_file(self, file_id: int, token: str | None = None) -> dict: ...
11
+ def download_file(self, file_id: int, token: str | None = None) -> tuple[bytes, str, str]: ... # (content_bytes, filename, media_type)
12
+ def delete_file(self, file_id: int, token: str | None = None) -> None: ...
13
13
 
14
- async def commit_file(
15
- self,
16
- file_id: int,
17
- client_id: str,
18
- token: str | None = None,
19
- ) -> None: ...
20
-
21
- async def list_files(
22
- self,
23
- limit: int = 10,
24
- offset: int = 0,
25
- filters: dict[str, Any] | None = None,
26
- sort: list[tuple[str, str]] | None = None,
27
- token: str | None = None,
28
- ) -> dict: ...
29
-
30
- async def get_file(
31
- self,
32
- file_id: int,
33
- token: str | None = None,
34
- ) -> dict: ...
35
-
36
- async def download_file(
37
- self,
38
- file_id: int,
39
- token: str | None = None,
40
- ) -> tuple[AsyncIterator[bytes], str, str]:
41
- ...
42
-
43
- async def delete_file(
44
- self,
45
- file_id: int,
46
- token: str | None = None,
47
- ) -> None: ...
14
+ @runtime_checkable
15
+ class AsyncFileServicePort(Protocol):
16
+ # ---- Async API (with a_-prefix) ----
17
+ async def upload_file(self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None) -> dict: ...
18
+ async def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None: ...
19
+ async def list_files(self, limit: int = 10, offset: int = 0, filters: dict[str, Any] | None = None, sort: list[tuple[str, str]] | None = None, token: str | None = None) -> dict: ...
20
+ async def get_file(self, file_id: int, token: str | None = None) -> dict: ...
21
+ async def download_file(self, file_id: int, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]: ... # (stream, filename, media_type)
22
+ async def delete_file(self, file_id: int, token: str | None = None) -> None: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.1.31
3
+ Version: 0.1.32
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,4 +1,5 @@
1
1
  nlbone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ nlbone/container.py,sha256=JLEiJSh7vZAl7kBY6bwqtL3IzHFOxhrfreaZDU6K9n0,663
2
3
  nlbone/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
4
  nlbone/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
5
  nlbone/adapters/auth/__init__.py,sha256=Eh9kWjY1I8vi17gK0oOzBLJwJX_GFuUcJIN7cLU6lJg,41
@@ -19,7 +20,8 @@ nlbone/adapters/db/sqlalchemy/query/ordering.py,sha256=THbuxZmoFZzJIa28KfDQ6ioS8
19
20
  nlbone/adapters/db/sqlalchemy/query/types.py,sha256=M2j6SOSyFkLuyXq4kvPYgZQYz5TKCBe3osoIIN1yfBI,287
20
21
  nlbone/adapters/http_clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
22
  nlbone/adapters/http_clients/email_gateway.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- nlbone/adapters/http_clients/uploadchi.py,sha256=tr-huESnVDg08FQh3QTFST7_JQz7dPShr2CAX9CQYLU,5737
23
+ nlbone/adapters/http_clients/uploadchi.py,sha256=oioyxoxyOXsiJWZaDPiSCerB4ZURO5zfjO1d19rUMQE,3826
24
+ nlbone/adapters/http_clients/uploadchi_async.py,sha256=0pL_7EV5T40v4Y8IW8rLgPrCEy_meO19YADaYvWpf_M,3499
23
25
  nlbone/adapters/messaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
26
  nlbone/adapters/messaging/redis.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
27
  nlbone/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -36,11 +38,9 @@ nlbone/core/domain/events.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,
36
38
  nlbone/core/domain/models.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
39
  nlbone/core/ports/__init__.py,sha256=J24ldeLIQ_AWqKpdNbtaEWq4-G4cyWx8XEG0Dt6keao,29
38
40
  nlbone/core/ports/auth.py,sha256=v2NiH8FzKXZ1MabzQxaK7AQvnt-GQindYIki90swTE4,492
39
- nlbone/core/ports/files.py,sha256=dYBMFsaesyIVaWxzTShGFMFzRZWbWcQtPdzhELY6t4A,1119
41
+ nlbone/core/ports/files.py,sha256=E_-bA5gURxiJpHWp9l9IliqCbf8P4BZ27ZynRHiAYRo,1739
40
42
  nlbone/core/ports/messaging.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
43
  nlbone/core/ports/repo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
- nlbone/di/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
- nlbone/di/container.py,sha256=nzWx8nctyBMAERoqjolt0vWS38H2JH2QhqVaLvAvF8U,488
44
44
  nlbone/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
45
  nlbone/interfaces/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
46
  nlbone/interfaces/api/exceptions.py,sha256=OczR1FND2nbCngwbfgaPrgDbDaz4Pc47mFSHgtL7tW8,313
@@ -57,7 +57,7 @@ nlbone/interfaces/jobs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
57
57
  nlbone/interfaces/jobs/sync_tokens.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
58
  nlbone/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  nlbone/utils/time.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
60
- nlbone-0.1.31.dist-info/METADATA,sha256=AQxSX36K4Z747tRRCrLWQ0qpuZyI586sVHKK00WAooo,2256
61
- nlbone-0.1.31.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
62
- nlbone-0.1.31.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
- nlbone-0.1.31.dist-info/RECORD,,
60
+ nlbone-0.1.32.dist-info/METADATA,sha256=qYFRUMJZdXjtxboUg3pAeeyUnMLtYrqXA9UlDyRMSck,2256
61
+ nlbone-0.1.32.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
62
+ nlbone-0.1.32.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
63
+ nlbone-0.1.32.dist-info/RECORD,,
nlbone/di/__init__.py DELETED
File without changes