nlbone 0.4.2__py3-none-any.whl → 0.4.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,40 @@
1
+ import threading
2
+ import time
3
+ from typing import Optional, Dict, Any
4
+
5
+ from nlbone.adapters.auth.keycloak import KeycloakAuthService
6
+
7
+
8
+ class ClientTokenProvider:
9
+ """Caches Keycloak client-credentials token and refreshes before expiry."""
10
+
11
+ def __init__(self, auth: KeycloakAuthService, *, skew_seconds: int = 30) -> None:
12
+ self._auth = auth
13
+ self._skew = skew_seconds
14
+ self._lock = threading.Lock()
15
+ self._token: Optional[str] = None # access_token
16
+ self._expires_at: float = 0.0 # epoch seconds
17
+
18
+ def _needs_refresh(self) -> bool:
19
+ return not self._token or time.time() >= (self._expires_at - self._skew)
20
+
21
+ def get_access_token(self) -> str:
22
+ """Return a valid access token; refresh if needed."""
23
+ if not self._needs_refresh():
24
+ return self._token
25
+
26
+ with self._lock:
27
+ if not self._needs_refresh():
28
+ return self._token
29
+
30
+ data: Dict[str, Any] = self._auth.get_client_token()
31
+ access_token = data.get("access_token")
32
+ if not access_token:
33
+ raise RuntimeError("Keycloak: missing access_token")
34
+ expires_in = int(data.get("expires_in", 60))
35
+ self._token = access_token
36
+ self._expires_at = time.time() + max(1, expires_in)
37
+ return self._token
38
+
39
+ def get_auth_header(self) -> str:
40
+ return f"Bearer {self.get_access_token()}"
@@ -22,6 +22,8 @@ def _get_ops_for(obj) -> set[str]:
22
22
 
23
23
 
24
24
  def _is_audit_disabled(obj) -> bool:
25
+ if not DEFAULT_ENABLED:
26
+ return True
25
27
  if getattr(obj, "__audit_disable__", False):
26
28
  return True
27
29
  if hasattr(obj, "__audit_enable__") and not getattr(obj, "__audit_enable__"):
@@ -7,6 +7,7 @@ from urllib.parse import urlparse, urlunparse
7
7
  import httpx
8
8
  import requests
9
9
 
10
+ from nlbone.adapters.auth.token_provider import ClientTokenProvider
10
11
  from nlbone.config.settings import get_settings
11
12
  from nlbone.core.ports.files import FileServicePort
12
13
 
@@ -30,7 +31,7 @@ def _auth_headers(token: str | None) -> dict[str, str]:
30
31
 
31
32
 
32
33
  def _build_list_query(
33
- limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
34
+ limit: int, offset: int, filters: dict[str, Any] | None, sort: list[tuple[str, str]] | None
34
35
  ) -> dict[str, Any]:
35
36
  q: dict[str, Any] = {"limit": limit, "offset": offset}
36
37
  if filters:
@@ -58,21 +59,23 @@ def _normalize_https_base(url: str) -> str:
58
59
 
59
60
  class UploadchiClient(FileServicePort):
60
61
  def __init__(
61
- self,
62
- base_url: Optional[str] = None,
63
- timeout_seconds: Optional[float] = None,
64
- client: httpx.Client | None = None,
62
+ self,
63
+ token_provider: ClientTokenProvider | None = None,
64
+ base_url: Optional[str] = None,
65
+ timeout_seconds: Optional[float] = None,
66
+ client: httpx.Client | None = None,
65
67
  ) -> None:
66
68
  s = get_settings()
67
69
  self._base_url = _normalize_https_base(base_url or str(s.UPLOADCHI_BASE_URL))
68
70
  self._timeout = timeout_seconds or float(s.HTTP_TIMEOUT_SECONDS)
69
71
  self._client = client or requests.session()
72
+ self._token_provider = token_provider
70
73
 
71
74
  def close(self) -> None:
72
75
  self._client.close()
73
76
 
74
77
  def upload_file(
75
- self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
78
+ self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
76
79
  ) -> dict:
77
80
  tok = _resolve_token(token)
78
81
  files = {"file": (filename, file_bytes)}
@@ -82,23 +85,35 @@ class UploadchiClient(FileServicePort):
82
85
  raise UploadchiError(r.status_code, r.text)
83
86
  return r.json()
84
87
 
85
- def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None:
88
+ def commit_file(self, file_id: str, token: str | None = None) -> None:
89
+ if not token and not self._token_provider:
90
+ raise UploadchiError(detail="token_provider is not provided", status=400)
86
91
  tok = _resolve_token(token)
87
92
  r = self._client.post(
88
93
  f"{self._base_url}/{file_id}/commit",
89
94
  headers=_auth_headers(tok),
90
- params={"client_id": client_id} if client_id else None,
95
+ )
96
+ if r.status_code not in (204, 200):
97
+ raise UploadchiError(r.status_code, r.text)
98
+
99
+ def rollback(self, file_id: str, token: str | None = None) -> None:
100
+ if not token and not self._token_provider:
101
+ raise UploadchiError(detail="token_provider is not provided", status=400)
102
+ tok = _resolve_token(token)
103
+ r = self._client.post(
104
+ f"{self._base_url}/{file_id}/rollback",
105
+ headers=_auth_headers(tok),
91
106
  )
92
107
  if r.status_code not in (204, 200):
93
108
  raise UploadchiError(r.status_code, r.text)
94
109
 
95
110
  def list_files(
96
- self,
97
- limit: int = 10,
98
- offset: int = 0,
99
- filters: dict[str, Any] | None = None,
100
- sort: list[tuple[str, str]] | None = None,
101
- token: str | None = None,
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,
102
117
  ) -> dict:
103
118
  tok = _resolve_token(token)
104
119
  q = _build_list_query(limit, offset, filters, sort)
@@ -107,14 +122,14 @@ class UploadchiClient(FileServicePort):
107
122
  raise UploadchiError(r.status_code, r.text)
108
123
  return r.json()
109
124
 
110
- def get_file(self, file_id: int, token: str | None = None) -> dict:
125
+ def get_file(self, file_id: str, token: str | None = None) -> dict:
111
126
  tok = _resolve_token(token)
112
127
  r = self._client.get(f"{self._base_url}/{file_id}", headers=_auth_headers(tok))
113
128
  if r.status_code >= 400:
114
129
  raise UploadchiError(r.status_code, r.text)
115
130
  return r.json()
116
131
 
117
- def download_file(self, file_id: int, token: str | None = None) -> tuple[bytes, str, str]:
132
+ def download_file(self, file_id: str, token: str | None = None) -> tuple[bytes, str, str]:
118
133
  tok = _resolve_token(token)
119
134
  r = self._client.get(f"{self._base_url}/{file_id}/download", headers=_auth_headers(tok))
120
135
  if r.status_code >= 400:
@@ -123,7 +138,7 @@ class UploadchiClient(FileServicePort):
123
138
  media_type = r.headers.get("content-type", "application/octet-stream")
124
139
  return r.content, filename, media_type
125
140
 
126
- def delete_file(self, file_id: int, token: str | None = None) -> None:
141
+ def delete_file(self, file_id: str, token: str | None = None) -> None:
127
142
  tok = _resolve_token(token)
128
143
  r = self._client.delete(f"{self._base_url}/{file_id}", headers=_auth_headers(tok))
129
144
  if r.status_code not in (204, 200):
@@ -8,14 +8,16 @@ from nlbone.config.settings import get_settings
8
8
  from nlbone.core.ports.files import AsyncFileServicePort
9
9
 
10
10
  from .uploadchi import UploadchiError, _auth_headers, _build_list_query, _filename_from_cd, _resolve_token
11
+ from ..auth.token_provider import ClientTokenProvider
11
12
 
12
13
 
13
14
  class UploadchiAsyncClient(AsyncFileServicePort):
14
15
  def __init__(
15
- self,
16
- base_url: Optional[str] = None,
17
- timeout_seconds: Optional[float] = None,
18
- client: httpx.AsyncClient | None = None,
16
+ self,
17
+ token_provider: ClientTokenProvider | None = None,
18
+ base_url: Optional[str] = None,
19
+ timeout_seconds: Optional[float] = None,
20
+ client: httpx.AsyncClient | None = None,
19
21
  ) -> None:
20
22
  s = get_settings()
21
23
  self._base_url = base_url or str(s.UPLOADCHI_BASE_URL)
@@ -23,12 +25,13 @@ class UploadchiAsyncClient(AsyncFileServicePort):
23
25
  self._client = client or httpx.AsyncClient(
24
26
  base_url=self._base_url, timeout=self._timeout, follow_redirects=True
25
27
  )
28
+ self._token_provider = token_provider
26
29
 
27
30
  async def aclose(self) -> None:
28
31
  await self._client.aclose()
29
32
 
30
33
  async def upload_file(
31
- self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
34
+ self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
32
35
  ) -> dict:
33
36
  tok = _resolve_token(token)
34
37
  files = {"file": (filename, file_bytes)}
@@ -38,21 +41,33 @@ class UploadchiAsyncClient(AsyncFileServicePort):
38
41
  raise UploadchiError(r.status_code, await r.aread())
39
42
  return r.json()
40
43
 
41
- async def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None:
44
+ async def commit_file(self, file_id: str, token: str | None = None) -> None:
45
+ if not token and not self._token_provider:
46
+ raise UploadchiError(detail="token_provider is not provided", status=400)
42
47
  tok = _resolve_token(token)
43
48
  r = await self._client.post(
44
- f"/{file_id}/commit", headers=_auth_headers(tok), params={"client_id": client_id} if client_id else None
49
+ f"/{file_id}/commit", headers=_auth_headers(tok)
50
+ )
51
+ if r.status_code not in (204, 200):
52
+ raise UploadchiError(r.status_code, await r.aread())
53
+
54
+ async def rollback(self, file_id: str, token: str | None = None) -> None:
55
+ if not token and not self._token_provider:
56
+ raise UploadchiError(detail="token_provider is not provided", status=400)
57
+ tok = _resolve_token(token)
58
+ r = await self._client.post(
59
+ f"/{file_id}/rollback", headers=_auth_headers(tok)
45
60
  )
46
61
  if r.status_code not in (204, 200):
47
62
  raise UploadchiError(r.status_code, await r.aread())
48
63
 
49
64
  async def list_files(
50
- self,
51
- limit: int = 10,
52
- offset: int = 0,
53
- filters: dict[str, Any] | None = None,
54
- sort: list[tuple[str, str]] | None = None,
55
- token: str | None = None,
65
+ self,
66
+ limit: int = 10,
67
+ offset: int = 0,
68
+ filters: dict[str, Any] | None = None,
69
+ sort: list[tuple[str, str]] | None = None,
70
+ token: str | None = None,
56
71
  ) -> dict:
57
72
  tok = _resolve_token(token)
58
73
  q = _build_list_query(limit, offset, filters, sort)
@@ -61,14 +76,14 @@ class UploadchiAsyncClient(AsyncFileServicePort):
61
76
  raise UploadchiError(r.status_code, await r.aread())
62
77
  return r.json()
63
78
 
64
- async def get_file(self, file_id: int, token: str | None = None) -> dict:
79
+ async def get_file(self, file_id: str, token: str | None = None) -> dict:
65
80
  tok = _resolve_token(token)
66
81
  r = await self._client.get(f"/{file_id}", headers=_auth_headers(tok))
67
82
  if r.status_code >= 400:
68
83
  raise UploadchiError(r.status_code, await r.aread())
69
84
  return r.json()
70
85
 
71
- async def download_file(self, file_id: int, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]:
86
+ async def download_file(self, file_id: str, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]:
72
87
  tok = _resolve_token(token)
73
88
  r = await self._client.get(f"/{file_id}/download", headers=_auth_headers(tok), stream=True)
74
89
  if r.status_code >= 400:
@@ -86,7 +101,7 @@ class UploadchiAsyncClient(AsyncFileServicePort):
86
101
 
87
102
  return _aiter(), filename, media_type
88
103
 
89
- async def delete_file(self, file_id: int, token: str | None = None) -> None:
104
+ async def delete_file(self, file_id: str, token: str | None = None) -> None:
90
105
  tok = _resolve_token(token)
91
106
  r = await self._client.delete(f"/{file_id}", headers=_auth_headers(tok))
92
107
  if r.status_code not in (204, 200):
nlbone/container.py CHANGED
@@ -5,6 +5,7 @@ from typing import Any, Mapping, Optional
5
5
  from dependency_injector import containers, providers
6
6
 
7
7
  from nlbone.adapters.auth.keycloak import KeycloakAuthService
8
+ from nlbone.adapters.auth.token_provider import ClientTokenProvider
8
9
  from nlbone.adapters.db.postgres import AsyncSqlAlchemyUnitOfWork, SqlAlchemyUnitOfWork
9
10
  from nlbone.adapters.db.postgres.engine import get_async_session_factory, get_sync_session_factory
10
11
  from nlbone.adapters.http_clients.uploadchi import UploadchiClient
@@ -29,8 +30,11 @@ class Container(containers.DeclarativeContainer):
29
30
 
30
31
  # --- Services ---
31
32
  auth: providers.Singleton[KeycloakAuthService] = providers.Singleton(KeycloakAuthService, settings=config)
32
- file_service: providers.Singleton[FileServicePort] = providers.Singleton(UploadchiClient)
33
- afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(UploadchiAsyncClient)
33
+ token_provider = providers.Singleton(ClientTokenProvider, auth=auth, skew_seconds=30)
34
+ file_service: providers.Singleton[FileServicePort] = providers.Singleton(UploadchiClient,
35
+ token_provider=token_provider)
36
+ afiles_service: providers.Singleton[AsyncFileServicePort] = providers.Singleton(UploadchiAsyncClient,
37
+ token_provider=token_provider)
34
38
 
35
39
 
36
40
  def create_container(settings: Optional[Any] = None) -> Container:
@@ -8,7 +8,8 @@ class FileServicePort(Protocol):
8
8
  def upload_file(
9
9
  self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
10
10
  ) -> dict: ...
11
- def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None: ...
11
+ def commit_file(self, file_id: str, token: str | None = None) -> None: ...
12
+ def rollback(self, file_id: str, token: str | None = None) -> None: ...
12
13
  def list_files(
13
14
  self,
14
15
  limit: int = 10,
@@ -17,9 +18,9 @@ class FileServicePort(Protocol):
17
18
  sort: list[tuple[str, str]] | None = None,
18
19
  token: str | None = None,
19
20
  ) -> dict: ...
20
- def get_file(self, file_id: int, token: str | None = None) -> dict: ...
21
- def download_file(self, file_id: int, token: str | None = None) -> tuple[bytes, str, str]: ...
22
- def delete_file(self, file_id: int, token: str | None = None) -> None: ...
21
+ def get_file(self, file_id: str, token: str | None = None) -> dict: ...
22
+ def download_file(self, file_id: str, token: str | None = None) -> tuple[bytes, str, str]: ...
23
+ def delete_file(self, file_id: str, token: str | None = None) -> None: ...
23
24
 
24
25
 
25
26
  @runtime_checkable
@@ -27,7 +28,8 @@ class AsyncFileServicePort(Protocol):
27
28
  async def upload_file(
28
29
  self, file_bytes: bytes, filename: str, params: dict[str, Any] | None = None, token: str | None = None
29
30
  ) -> dict: ...
30
- async def commit_file(self, file_id: int, client_id: str, token: str | None = None) -> None: ...
31
+ async def commit_file(self, file_id: str, token: str | None = None) -> None: ...
32
+ async def rollback(self, file_id: str, token: str | None = None) -> None: ...
31
33
  async def list_files(
32
34
  self,
33
35
  limit: int = 10,
@@ -36,6 +38,6 @@ class AsyncFileServicePort(Protocol):
36
38
  sort: list[tuple[str, str]] | None = None,
37
39
  token: str | None = None,
38
40
  ) -> dict: ...
39
- async def get_file(self, file_id: int, token: str | None = None) -> dict: ...
40
- async def download_file(self, file_id: int, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]: ...
41
- async def delete_file(self, file_id: int, token: str | None = None) -> None: ...
41
+ async def get_file(self, file_id: str, token: str | None = None) -> dict: ...
42
+ async def download_file(self, file_id: str, token: str | None = None) -> tuple[AsyncIterator[bytes], str, str]: ...
43
+ async def delete_file(self, file_id: str, token: str | None = None) -> None: ...
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nlbone
3
- Version: 0.4.2
3
+ Version: 0.4.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
@@ -79,4 +79,8 @@ async def main():
79
79
 
80
80
 
81
81
  anyio.run(main)
82
- ```
82
+ ```
83
+
84
+ ## 📦 Used In
85
+ - **Explore**
86
+ - **Pricing**
@@ -1,12 +1,13 @@
1
1
  nlbone/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- nlbone/container.py,sha256=Jw5XMBmmrgkd_iZwItVfGCj1vkOiedpn8eIdYHpjWsk,2100
2
+ nlbone/container.py,sha256=wmamsFU0Be6DlEqhLW30J2w4zVCwNlcZDd7HxkzWil0,2481
3
3
  nlbone/types.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  nlbone/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  nlbone/adapters/auth/__init__.py,sha256=hkDHvsFhw_UiOHG9ZSMqjiAhK4wumEforitveSZswVw,42
6
6
  nlbone/adapters/auth/keycloak.py,sha256=dfAxODiARfR8y3FKoWNo9fjfb6QyWd_Qr7AbJ0E78AM,2729
7
+ nlbone/adapters/auth/token_provider.py,sha256=NhqjqTUsoZO4gbK-cybs0OkKydFN7CPTxAiypEw081o,1433
7
8
  nlbone/adapters/db/__init__.py,sha256=saW-wN4E0NZ2_ldi-nrm5AgsH7EULNSa62lYMwfy1oo,252
8
9
  nlbone/adapters/db/postgres/__init__.py,sha256=6JYJH0xZs3aR-zuyMpRhsdzFugmqz8nprwTQLprqhZc,313
9
- nlbone/adapters/db/postgres/audit.py,sha256=OYPQTDC0p27D3X8iQ-g3fu7Cno3bDm8egodLMPDcn3k,4627
10
+ nlbone/adapters/db/postgres/audit.py,sha256=zFzL-pXmfjcp5YLx6vBYczprsJjEPxSYKhQNR3WjKL0,4675
10
11
  nlbone/adapters/db/postgres/base.py,sha256=kha9xmklzhuQAK8QEkNBn-mAHq8dUKbOM-3abaBpWmQ,71
11
12
  nlbone/adapters/db/postgres/engine.py,sha256=UCegauVB1gvo42ThytYnn5VIcQBwR-5xhcXYFApRFNk,3448
12
13
  nlbone/adapters/db/postgres/query_builder.py,sha256=U5pqpCfJKuMIxIEHyodoHuPgE8jf53slC1ScKZR5xa4,8653
@@ -17,8 +18,8 @@ nlbone/adapters/db/redis/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZ
17
18
  nlbone/adapters/db/redis/client.py,sha256=XAKcmU0lpPvWPMS0fChVQ3iSJfHV1g4bMOCgJaj2bCI,512
18
19
  nlbone/adapters/http_clients/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
20
  nlbone/adapters/http_clients/email_gateway.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- nlbone/adapters/http_clients/uploadchi.py,sha256=BGdGC-p8EmXYn29da577Kas2CzZVDAb9XJ9xx2LJioY,4713
21
- nlbone/adapters/http_clients/uploadchi_async.py,sha256=hBx0jzYYZAX1DCkImZ98zdUob8D9PQH7jykDXIjwG9I,3866
21
+ nlbone/adapters/http_clients/uploadchi.py,sha256=iMLbXUXqi60gQMEI7wfaqWG0G7p9fQN7otT9c8npnLQ,5470
22
+ nlbone/adapters/http_clients/uploadchi_async.py,sha256=wmb2XIUNkowh48GJwttNZ_STgsFQdGKKRZc_luYNCu8,4609
22
23
  nlbone/adapters/messaging/__init__.py,sha256=UDAwu3s-JQmOZjWz2Nu0SgHhnkbeOhKDH_zLD75oWMY,40
23
24
  nlbone/adapters/messaging/event_bus.py,sha256=w-NPwDiPMLFPU_enRQCtfQXOALsXfg31u57R8sG_-1U,781
24
25
  nlbone/adapters/messaging/redis.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -41,7 +42,7 @@ nlbone/core/domain/models.py,sha256=e2ig7PMBBpmc8pdHLNMnXhucMXr9OUq-G7bKGTq9Qj0,
41
42
  nlbone/core/ports/__init__.py,sha256=gx-Ubj7h-1vvnu56sNnRqmer7HHfW3rX2WLl-0AX5U0,214
42
43
  nlbone/core/ports/auth.py,sha256=Gh0yQsxx2OD6pDH2_p-khsA-bVoypP1juuqMoSfjZUo,493
43
44
  nlbone/core/ports/event_bus.py,sha256=_Om1GOOT-F325oV6_LJXtLdx4vu5i7KrpTDD3qPJXU0,325
44
- nlbone/core/ports/files.py,sha256=1k-Vm0ld89EnFK2wybSXIJm5gQNpeuO92PD7d4VMh8s,1737
45
+ nlbone/core/ports/files.py,sha256=7Ov2ITYRpPwwDTZGCeNVISg8e3A9l08jbOgpTImgfK8,1863
45
46
  nlbone/core/ports/messaging.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
46
47
  nlbone/core/ports/repo.py,sha256=zOw8CTMAu5DKKy2wZpT3_6JWWjaJCDt7q4dOiJYrCOQ,651
47
48
  nlbone/core/ports/uow.py,sha256=SmBdRf0NvSdIjQ3Le1QGz8kNGBk7jgNHtNguvXRwmgs,557
@@ -71,8 +72,8 @@ nlbone/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
71
72
  nlbone/utils/context.py,sha256=MmclJ24BG2uvSTg1IK7J-Da9BhVFDQ5ag4Ggs2FF1_w,1600
72
73
  nlbone/utils/redactor.py,sha256=JbbPs2Qtnz0zHN85BGPYQNWwBigXMSzmMEmmZZOTs_U,1277
73
74
  nlbone/utils/time.py,sha256=6e0A4_hG1rYDCrWoOklEGVJstBf8j9XSSTT7VNV2K9Y,1272
74
- nlbone-0.4.2.dist-info/METADATA,sha256=nriPe98XsiI5atrlh8I13Z1dFPW-sRP_iF7B658Q0GE,2117
75
- nlbone-0.4.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
76
- nlbone-0.4.2.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
77
- nlbone-0.4.2.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- nlbone-0.4.2.dist-info/RECORD,,
75
+ nlbone-0.4.3.dist-info/METADATA,sha256=dKKof0RuBfc3ONT-R1n5qD_oJXaE4BIhqcezwx8NH8Q,2163
76
+ nlbone-0.4.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
77
+ nlbone-0.4.3.dist-info/entry_points.txt,sha256=CpIL45t5nbhl1dGQPhfIIDfqqak3teK0SxPGBBr7YCk,59
78
+ nlbone-0.4.3.dist-info/licenses/LICENSE,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
79
+ nlbone-0.4.3.dist-info/RECORD,,
File without changes