investify-utils 2.0.0a2__py3-none-any.whl → 2.0.0a4__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.

Potentially problematic release.


This version of investify-utils might be problematic. Click here for more details.

@@ -8,7 +8,18 @@ Async client (asyncpg):
8
8
  from investify_utils.postgres import AsyncPostgresClient
9
9
  """
10
10
 
11
- from investify_utils.postgres.sync_client import PostgresClient
12
- from investify_utils.postgres.async_client import AsyncPostgresClient
11
+
12
+ def __getattr__(name: str):
13
+ """Lazy import to avoid loading dependencies for unused clients."""
14
+ if name == "PostgresClient":
15
+ from investify_utils.postgres.sync_client import PostgresClient
16
+
17
+ return PostgresClient
18
+ if name == "AsyncPostgresClient":
19
+ from investify_utils.postgres.async_client import AsyncPostgresClient
20
+
21
+ return AsyncPostgresClient
22
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
23
+
13
24
 
14
25
  __all__ = ["PostgresClient", "AsyncPostgresClient"]
@@ -0,0 +1,18 @@
1
+ """
2
+ S3-compatible object storage client.
3
+
4
+ Usage:
5
+ from investify_utils.s3 import S3Client
6
+ """
7
+
8
+
9
+ def __getattr__(name: str):
10
+ """Lazy import to avoid loading boto3 if not needed."""
11
+ if name == "S3Client":
12
+ from investify_utils.s3.sync_client import S3Client
13
+
14
+ return S3Client
15
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
16
+
17
+
18
+ __all__ = ["S3Client"]
@@ -0,0 +1,226 @@
1
+ """
2
+ S3-compatible object storage client using boto3.
3
+
4
+ Features:
5
+ - Works with AWS S3, Ceph RGW, MinIO, and other S3-compatible services
6
+ - Lazy client initialization (safe for Celery prefork)
7
+ - Common operations: upload, download, get, put, delete, list
8
+
9
+ Usage:
10
+ from investify_utils.s3 import S3Client
11
+
12
+ client = S3Client(
13
+ endpoint_url="https://s3.example.com",
14
+ access_key="access_key",
15
+ secret_key="secret_key",
16
+ )
17
+
18
+ # Upload file
19
+ client.upload_file("local.pdf", bucket="my-bucket", key="remote.pdf")
20
+
21
+ # Get object as bytes
22
+ data = client.get_object("my-bucket", "remote.pdf")
23
+
24
+ # Put object from bytes/string
25
+ client.put_object("my-bucket", "file.txt", b"content", content_type="text/plain")
26
+ """
27
+
28
+ import io
29
+ import os
30
+ from typing import IO
31
+
32
+ import boto3
33
+ from botocore.config import Config
34
+ from botocore.exceptions import ClientError
35
+
36
+
37
+ class S3Client:
38
+ """
39
+ S3-compatible object storage client with lazy initialization.
40
+
41
+ Args:
42
+ endpoint_url: S3 endpoint URL (e.g., https://s3.amazonaws.com)
43
+ access_key: AWS access key ID
44
+ secret_key: AWS secret access key
45
+ region: AWS region (default: None)
46
+ **kwargs: Additional boto3 client options
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ endpoint_url: str,
52
+ access_key: str,
53
+ secret_key: str,
54
+ region: str | None = None,
55
+ **kwargs,
56
+ ):
57
+ self._endpoint_url = endpoint_url
58
+ self._access_key = access_key
59
+ self._secret_key = secret_key
60
+ self._region = region
61
+ self._kwargs = kwargs
62
+ self._client = None
63
+
64
+ @property
65
+ def client(self):
66
+ """Lazy client initialization - created on first access."""
67
+ if self._client is None:
68
+ self._client = boto3.client(
69
+ "s3",
70
+ endpoint_url=self._endpoint_url,
71
+ aws_access_key_id=self._access_key,
72
+ aws_secret_access_key=self._secret_key,
73
+ region_name=self._region,
74
+ config=Config(signature_version="s3v4"),
75
+ **self._kwargs,
76
+ )
77
+ return self._client
78
+
79
+ def list_buckets(self) -> list[str]:
80
+ """List all buckets."""
81
+ response = self.client.list_buckets()
82
+ return [bucket["Name"] for bucket in response["Buckets"]]
83
+
84
+ def list_objects(
85
+ self,
86
+ bucket: str,
87
+ prefix: str = "",
88
+ max_keys: int | None = None,
89
+ ) -> list[dict]:
90
+ """
91
+ List objects in a bucket with optional prefix filter.
92
+
93
+ Args:
94
+ bucket: Bucket name
95
+ prefix: Filter objects by prefix (e.g., "folder/")
96
+ max_keys: Maximum number of objects to return (None = all)
97
+
98
+ Returns:
99
+ List of object metadata dicts with keys: Key, Size, LastModified
100
+ """
101
+ objects = []
102
+ paginator = self.client.get_paginator("list_objects_v2")
103
+
104
+ for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
105
+ for obj in page.get("Contents", []):
106
+ objects.append({
107
+ "Key": obj["Key"],
108
+ "Size": obj["Size"],
109
+ "LastModified": obj["LastModified"],
110
+ })
111
+ if max_keys and len(objects) >= max_keys:
112
+ return objects
113
+
114
+ return objects
115
+
116
+ def upload_file(self, file_path: str, bucket: str, key: str | None = None) -> None:
117
+ """
118
+ Upload a local file to S3.
119
+
120
+ Args:
121
+ file_path: Local file path
122
+ bucket: Bucket name
123
+ key: Object key (default: basename of file_path)
124
+ """
125
+ if key is None:
126
+ key = os.path.basename(file_path)
127
+ self.client.upload_file(file_path, bucket, key)
128
+
129
+ def download_file(self, bucket: str, key: str, file_path: str) -> None:
130
+ """
131
+ Download an object to a local file.
132
+
133
+ Args:
134
+ bucket: Bucket name
135
+ key: Object key
136
+ file_path: Local file path to save to
137
+ """
138
+ self.client.download_file(bucket, key, file_path)
139
+
140
+ def get_object(self, bucket: str, key: str) -> IO[bytes]:
141
+ """
142
+ Get object content as a file-like BytesIO object.
143
+
144
+ Args:
145
+ bucket: Bucket name
146
+ key: Object key
147
+
148
+ Returns:
149
+ BytesIO object with object content
150
+ """
151
+ response = self.client.get_object(Bucket=bucket, Key=key)
152
+ return io.BytesIO(response["Body"].read())
153
+
154
+ def put_object(
155
+ self,
156
+ bucket: str,
157
+ key: str,
158
+ data: str | bytes | IO[bytes],
159
+ content_type: str | None = None,
160
+ content_disposition: str | None = None,
161
+ ) -> None:
162
+ """
163
+ Upload data directly to S3.
164
+
165
+ Args:
166
+ bucket: Bucket name
167
+ key: Object key
168
+ data: Content as string, bytes, or file-like object
169
+ content_type: MIME type (e.g., "application/pdf")
170
+ content_disposition: Content-Disposition header value
171
+ """
172
+ params = {"Bucket": bucket, "Key": key, "Body": data}
173
+ if content_type:
174
+ params["ContentType"] = content_type
175
+ if content_disposition:
176
+ params["ContentDisposition"] = content_disposition
177
+ self.client.put_object(**params)
178
+
179
+ def delete_object(self, bucket: str, key: str) -> None:
180
+ """Delete a single object."""
181
+ self.client.delete_object(Bucket=bucket, Key=key)
182
+
183
+ def delete_prefix(self, bucket: str, prefix: str) -> int:
184
+ """
185
+ Delete all objects with a given prefix.
186
+
187
+ Args:
188
+ bucket: Bucket name
189
+ prefix: Prefix to match (e.g., "folder/" deletes all in folder)
190
+
191
+ Returns:
192
+ Number of objects deleted
193
+ """
194
+ deleted_count = 0
195
+ paginator = self.client.get_paginator("list_objects_v2")
196
+
197
+ for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
198
+ contents = page.get("Contents", [])
199
+ if not contents:
200
+ continue
201
+
202
+ delete_keys = [{"Key": obj["Key"]} for obj in contents]
203
+ self.client.delete_objects(Bucket=bucket, Delete={"Objects": delete_keys})
204
+ deleted_count += len(delete_keys)
205
+
206
+ return deleted_count
207
+
208
+ def exists(self, bucket: str, key: str) -> bool:
209
+ """
210
+ Check if an object exists.
211
+
212
+ Args:
213
+ bucket: Bucket name
214
+ key: Object key
215
+
216
+ Returns:
217
+ True if object exists, False otherwise
218
+ """
219
+ try:
220
+ self.client.head_object(Bucket=bucket, Key=key)
221
+ return True
222
+ except ClientError as e:
223
+ if e.response["Error"]["Code"] == "404":
224
+ return False
225
+ raise
226
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: investify-utils
3
- Version: 2.0.0a2
3
+ Version: 2.0.0a4
4
4
  Summary: Shared utilities for Investify services
5
5
  Author-Email: Investify <dev@investify.vn>
6
6
  License: MIT
@@ -20,8 +20,8 @@ Provides-Extra: postgres-async
20
20
  Requires-Dist: pandas>=2.0; extra == "postgres-async"
21
21
  Requires-Dist: sqlalchemy>=2.0; extra == "postgres-async"
22
22
  Requires-Dist: asyncpg>=0.29; extra == "postgres-async"
23
- Provides-Extra: postgres-all
24
- Requires-Dist: investify-utils[postgres,postgres-async]; extra == "postgres-all"
23
+ Provides-Extra: s3
24
+ Requires-Dist: boto3>=1.34; extra == "s3"
25
25
  Provides-Extra: dev
26
26
  Requires-Dist: pytest; extra == "dev"
27
27
  Requires-Dist: pytest-asyncio; extra == "dev"
@@ -40,9 +40,6 @@ pip install investify-utils[postgres]
40
40
 
41
41
  # Async PostgreSQL client (asyncpg + SQLAlchemy)
42
42
  pip install investify-utils[postgres-async]
43
-
44
- # Both clients
45
- pip install investify-utils[postgres-all]
46
43
  ```
47
44
 
48
45
  ## PostgreSQL Clients
@@ -0,0 +1,10 @@
1
+ investify_utils-2.0.0a4.dist-info/METADATA,sha256=d5A1HPVk6BaGWCRsdvLsZFW4rRUM62AAS1b0kny6GGU,3453
2
+ investify_utils-2.0.0a4.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
+ investify_utils-2.0.0a4.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
+ investify_utils/__init__.py,sha256=-Gn2EAJfZ5BRlh7DMSSSvOeDK6JTJq9LWTEekieh3WY,427
5
+ investify_utils/postgres/__init__.py,sha256=j4CfUw7U58vWstmxaKQuPkLVbKkOioC4Bc7_knllL_Y,737
6
+ investify_utils/postgres/async_client.py,sha256=M3F7-AsBJ43WWhfknnvTK9BeiYAyO0R6n-XY4DOnyFA,3168
7
+ investify_utils/postgres/sync_client.py,sha256=1mozgrNGUUKCR2ETAr9G9dzvW8uG_TmSqcbA63tRpM8,6507
8
+ investify_utils/s3/__init__.py,sha256=0YX-efJTP38Q5XMCyr7u-fXMjCJXkAR7dG817quTns8,399
9
+ investify_utils/s3/sync_client.py,sha256=fj6ejhAu06BUBRe2pnceKaNGhbPM79Xf47geL0DB-i0,6771
10
+ investify_utils-2.0.0a4.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- investify_utils-2.0.0a2.dist-info/METADATA,sha256=zNHqI9fWgl3lOf4VI_SKuWIRfuQhKFHR483rMmNpAvQ,3560
2
- investify_utils-2.0.0a2.dist-info/WHEEL,sha256=tsUv_t7BDeJeRHaSrczbGeuK-TtDpGsWi_JfpzD255I,90
3
- investify_utils-2.0.0a2.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34
4
- investify_utils/__init__.py,sha256=-Gn2EAJfZ5BRlh7DMSSSvOeDK6JTJq9LWTEekieh3WY,427
5
- investify_utils/postgres/__init__.py,sha256=RYuBFyj0CfpWfHgBv2NUIiK1pGJERud-IWrFD5S6Wus,406
6
- investify_utils/postgres/async_client.py,sha256=M3F7-AsBJ43WWhfknnvTK9BeiYAyO0R6n-XY4DOnyFA,3168
7
- investify_utils/postgres/sync_client.py,sha256=1mozgrNGUUKCR2ETAr9G9dzvW8uG_TmSqcbA63tRpM8,6507
8
- investify_utils-2.0.0a2.dist-info/RECORD,,