investify-utils 2.0.0a2__tar.gz → 2.0.0a4__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.
Potentially problematic release.
This version of investify-utils might be problematic. Click here for more details.
- {investify_utils-2.0.0a2 → investify_utils-2.0.0a4}/PKG-INFO +3 -6
- {investify_utils-2.0.0a2 → investify_utils-2.0.0a4}/README.md +0 -3
- investify_utils-2.0.0a4/investify_utils/postgres/__init__.py +25 -0
- investify_utils-2.0.0a4/investify_utils/s3/__init__.py +18 -0
- investify_utils-2.0.0a4/investify_utils/s3/sync_client.py +226 -0
- {investify_utils-2.0.0a2 → investify_utils-2.0.0a4}/pyproject.toml +3 -3
- investify_utils-2.0.0a2/investify_utils/postgres/__init__.py +0 -14
- {investify_utils-2.0.0a2 → investify_utils-2.0.0a4}/investify_utils/__init__.py +0 -0
- {investify_utils-2.0.0a2 → investify_utils-2.0.0a4}/investify_utils/postgres/async_client.py +0 -0
- {investify_utils-2.0.0a2 → investify_utils-2.0.0a4}/investify_utils/postgres/sync_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: investify-utils
|
|
3
|
-
Version: 2.0.
|
|
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:
|
|
24
|
-
Requires-Dist:
|
|
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,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PostgreSQL clients for Investify services.
|
|
3
|
+
|
|
4
|
+
Sync client (psycopg3):
|
|
5
|
+
from investify_utils.postgres import PostgresClient
|
|
6
|
+
|
|
7
|
+
Async client (asyncpg):
|
|
8
|
+
from investify_utils.postgres import AsyncPostgresClient
|
|
9
|
+
"""
|
|
10
|
+
|
|
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
|
+
|
|
24
|
+
|
|
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
|
+
|
|
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
|
|
|
6
6
|
|
|
7
7
|
[project]
|
|
8
8
|
name = "investify-utils"
|
|
9
|
-
version = "2.0.
|
|
9
|
+
version = "2.0.0a4"
|
|
10
10
|
description = "Shared utilities for Investify services"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.12"
|
|
@@ -40,8 +40,8 @@ postgres-async = [
|
|
|
40
40
|
"sqlalchemy>=2.0",
|
|
41
41
|
"asyncpg>=0.29",
|
|
42
42
|
]
|
|
43
|
-
|
|
44
|
-
"
|
|
43
|
+
s3 = [
|
|
44
|
+
"boto3>=1.34",
|
|
45
45
|
]
|
|
46
46
|
dev = [
|
|
47
47
|
"pytest",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
PostgreSQL clients for Investify services.
|
|
3
|
-
|
|
4
|
-
Sync client (psycopg3):
|
|
5
|
-
from investify_utils.postgres import PostgresClient
|
|
6
|
-
|
|
7
|
-
Async client (asyncpg):
|
|
8
|
-
from investify_utils.postgres import AsyncPostgresClient
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from investify_utils.postgres.sync_client import PostgresClient
|
|
12
|
-
from investify_utils.postgres.async_client import AsyncPostgresClient
|
|
13
|
-
|
|
14
|
-
__all__ = ["PostgresClient", "AsyncPostgresClient"]
|
|
File without changes
|
{investify_utils-2.0.0a2 → investify_utils-2.0.0a4}/investify_utils/postgres/async_client.py
RENAMED
|
File without changes
|
|
File without changes
|