tamar-file-hub-client 0.0.1__py3-none-any.whl → 0.0.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.
- file_hub_client/__init__.py +39 -0
- file_hub_client/client.py +43 -6
- file_hub_client/rpc/async_client.py +91 -11
- file_hub_client/rpc/gen/taple_service_pb2.py +225 -0
- file_hub_client/rpc/gen/taple_service_pb2_grpc.py +1626 -0
- file_hub_client/rpc/generate_grpc.py +2 -2
- file_hub_client/rpc/interceptors.py +550 -0
- file_hub_client/rpc/protos/taple_service.proto +874 -0
- file_hub_client/rpc/sync_client.py +91 -9
- file_hub_client/schemas/__init__.py +60 -0
- file_hub_client/schemas/taple.py +413 -0
- file_hub_client/services/__init__.py +5 -0
- file_hub_client/services/file/async_blob_service.py +558 -482
- file_hub_client/services/file/async_file_service.py +18 -9
- file_hub_client/services/file/base_file_service.py +19 -6
- file_hub_client/services/file/sync_blob_service.py +556 -478
- file_hub_client/services/file/sync_file_service.py +18 -9
- file_hub_client/services/folder/async_folder_service.py +20 -11
- file_hub_client/services/folder/sync_folder_service.py +20 -11
- file_hub_client/services/taple/__init__.py +10 -0
- file_hub_client/services/taple/async_taple_service.py +2281 -0
- file_hub_client/services/taple/base_taple_service.py +353 -0
- file_hub_client/services/taple/idempotent_taple_mixin.py +142 -0
- file_hub_client/services/taple/sync_taple_service.py +2256 -0
- file_hub_client/utils/__init__.py +43 -1
- file_hub_client/utils/file_utils.py +59 -11
- file_hub_client/utils/idempotency.py +196 -0
- file_hub_client/utils/logging.py +315 -0
- file_hub_client/utils/retry.py +241 -2
- file_hub_client/utils/smart_retry.py +403 -0
- tamar_file_hub_client-0.0.3.dist-info/METADATA +2050 -0
- tamar_file_hub_client-0.0.3.dist-info/RECORD +57 -0
- tamar_file_hub_client-0.0.1.dist-info/METADATA +0 -874
- tamar_file_hub_client-0.0.1.dist-info/RECORD +0 -44
- {tamar_file_hub_client-0.0.1.dist-info → tamar_file_hub_client-0.0.3.dist-info}/WHEEL +0 -0
- {tamar_file_hub_client-0.0.1.dist-info → tamar_file_hub_client-0.0.3.dist-info}/top_level.txt +0 -0
file_hub_client/__init__.py
CHANGED
@@ -45,6 +45,26 @@ from .schemas import (
|
|
45
45
|
UserContext,
|
46
46
|
RequestContext,
|
47
47
|
FullContext,
|
48
|
+
# Taple 相关模型
|
49
|
+
Table,
|
50
|
+
Sheet,
|
51
|
+
Column,
|
52
|
+
Row,
|
53
|
+
Cell,
|
54
|
+
MergedCell,
|
55
|
+
TableView,
|
56
|
+
CellUpdate,
|
57
|
+
TableViewResponse,
|
58
|
+
BatchCreateTableViewResult,
|
59
|
+
BatchCreateTableViewsResponse,
|
60
|
+
ListTableViewsResponse,
|
61
|
+
)
|
62
|
+
|
63
|
+
# 幂等性工具
|
64
|
+
from .utils.idempotency import (
|
65
|
+
IdempotencyKeyGenerator,
|
66
|
+
IdempotencyManager,
|
67
|
+
generate_idempotency_key,
|
48
68
|
)
|
49
69
|
|
50
70
|
__all__ = [
|
@@ -68,6 +88,20 @@ __all__ = [
|
|
68
88
|
"FolderInfo",
|
69
89
|
"FolderListResponse",
|
70
90
|
|
91
|
+
# Taple 相关模型
|
92
|
+
"Table",
|
93
|
+
"Sheet",
|
94
|
+
"Column",
|
95
|
+
"Row",
|
96
|
+
"Cell",
|
97
|
+
"MergedCell",
|
98
|
+
"TableView",
|
99
|
+
"CellUpdate",
|
100
|
+
"TableViewResponse",
|
101
|
+
"BatchCreateTableViewResult",
|
102
|
+
"BatchCreateTableViewsResponse",
|
103
|
+
"ListTableViewsResponse",
|
104
|
+
|
71
105
|
# 枚举
|
72
106
|
"Role",
|
73
107
|
"UploadMode",
|
@@ -85,4 +119,9 @@ __all__ = [
|
|
85
119
|
"ConnectionError",
|
86
120
|
"TimeoutError",
|
87
121
|
"PermissionError",
|
122
|
+
|
123
|
+
# 幂等性工具
|
124
|
+
"IdempotencyKeyGenerator",
|
125
|
+
"IdempotencyManager",
|
126
|
+
"generate_idempotency_key",
|
88
127
|
]
|
file_hub_client/client.py
CHANGED
@@ -15,9 +15,11 @@ from .services import (
|
|
15
15
|
AsyncBlobService,
|
16
16
|
AsyncFileService,
|
17
17
|
AsyncFolderService,
|
18
|
+
AsyncTapleService,
|
18
19
|
SyncBlobService,
|
19
20
|
SyncFileService,
|
20
21
|
SyncFolderService,
|
22
|
+
SyncTapleService,
|
21
23
|
)
|
22
24
|
from .schemas.context import UserContext, RequestContext
|
23
25
|
|
@@ -37,13 +39,15 @@ class AsyncTamarFileHubClient:
|
|
37
39
|
default_metadata: Optional[Dict[str, str]] = None,
|
38
40
|
user_context: Optional[UserContext] = None,
|
39
41
|
request_context: Optional[RequestContext] = None,
|
42
|
+
enable_logging: bool = True,
|
43
|
+
log_level: str = "INFO",
|
40
44
|
):
|
41
45
|
"""
|
42
46
|
初始化客户端
|
43
47
|
|
44
48
|
Args:
|
45
|
-
host:
|
46
|
-
port:
|
49
|
+
host: 服务器地址,可以是域名或IP(默认从环境变量 FILE_HUB_HOST 读取,否则使用 localhost)
|
50
|
+
port: 服务器端口,可选参数,不指定时直接使用host作为完整地址(默认从环境变量 FILE_HUB_PORT 读取)
|
47
51
|
secure: 是否使用TLS(默认从环境变量 FILE_HUB_SECURE 读取,否则使用 False)
|
48
52
|
credentials: 认证凭据
|
49
53
|
auto_connect: 是否自动连接(默认 True)
|
@@ -63,7 +67,12 @@ class AsyncTamarFileHubClient:
|
|
63
67
|
"""
|
64
68
|
# 从环境变量或参数获取配置
|
65
69
|
self._host = host or os.getenv('FILE_HUB_HOST', 'localhost')
|
66
|
-
|
70
|
+
# 端口处理:如果有环境变量且不为空,则使用;否则使用参数值(可能为None)
|
71
|
+
env_port = os.getenv('FILE_HUB_PORT')
|
72
|
+
if env_port:
|
73
|
+
self._port = int(env_port)
|
74
|
+
else:
|
75
|
+
self._port = port
|
67
76
|
self._secure = secure if secure is not None else os.getenv('FILE_HUB_SECURE', 'false').lower() == 'true'
|
68
77
|
self._retry_count = retry_count or int(os.getenv('FILE_HUB_RETRY_COUNT', '3'))
|
69
78
|
self._retry_delay = retry_delay or float(os.getenv('FILE_HUB_RETRY_DELAY', '1.0'))
|
@@ -87,10 +96,13 @@ class AsyncTamarFileHubClient:
|
|
87
96
|
default_metadata=default_metadata,
|
88
97
|
user_context=user_context,
|
89
98
|
request_context=request_context,
|
99
|
+
enable_logging=enable_logging,
|
100
|
+
log_level=log_level,
|
90
101
|
)
|
91
102
|
self._blob_service = None
|
92
103
|
self._file_service = None
|
93
104
|
self._folder_service = None
|
105
|
+
self._taple_service = None
|
94
106
|
self._auto_connect = auto_connect
|
95
107
|
self._connected = False
|
96
108
|
|
@@ -120,6 +132,13 @@ class AsyncTamarFileHubClient:
|
|
120
132
|
self._folder_service = AsyncFolderService(self._client)
|
121
133
|
return self._folder_service
|
122
134
|
|
135
|
+
@property
|
136
|
+
def taples(self) -> AsyncTapleService:
|
137
|
+
"""获取 Taple 服务"""
|
138
|
+
if self._taple_service is None:
|
139
|
+
self._taple_service = AsyncTapleService(self._client)
|
140
|
+
return self._taple_service
|
141
|
+
|
123
142
|
def set_user_context(self, org_id: str, user_id: str, role: Role = Role.ACCOUNT, actor_id: Optional[str] = None):
|
124
143
|
"""
|
125
144
|
设置用户上下文信息
|
@@ -207,13 +226,15 @@ class TamarFileHubClient:
|
|
207
226
|
default_metadata: Optional[Dict[str, str]] = None,
|
208
227
|
user_context: Optional[UserContext] = None,
|
209
228
|
request_context: Optional[RequestContext] = None,
|
229
|
+
enable_logging: bool = True,
|
230
|
+
log_level: str = "INFO",
|
210
231
|
):
|
211
232
|
"""
|
212
233
|
初始化客户端
|
213
234
|
|
214
235
|
Args:
|
215
|
-
host:
|
216
|
-
port:
|
236
|
+
host: 服务器地址,可以是域名或IP(默认从环境变量 FILE_HUB_HOST 读取,否则使用 localhost)
|
237
|
+
port: 服务器端口,可选参数,不指定时直接使用host作为完整地址(默认从环境变量 FILE_HUB_PORT 读取)
|
217
238
|
secure: 是否使用TLS(默认从环境变量 FILE_HUB_SECURE 读取,否则使用 False)
|
218
239
|
credentials: 认证凭据
|
219
240
|
auto_connect: 是否自动连接(默认 True)
|
@@ -233,7 +254,12 @@ class TamarFileHubClient:
|
|
233
254
|
"""
|
234
255
|
# 从环境变量或参数获取配置
|
235
256
|
self._host = host or os.getenv('FILE_HUB_HOST', 'localhost')
|
236
|
-
|
257
|
+
# 端口处理:如果有环境变量且不为空,则使用;否则使用参数值(可能为None)
|
258
|
+
env_port = os.getenv('FILE_HUB_PORT')
|
259
|
+
if env_port:
|
260
|
+
self._port = int(env_port)
|
261
|
+
else:
|
262
|
+
self._port = port
|
237
263
|
self._secure = secure if secure is not None else os.getenv('FILE_HUB_SECURE', 'false').lower() == 'true'
|
238
264
|
self._retry_count = retry_count or int(os.getenv('FILE_HUB_RETRY_COUNT', '3'))
|
239
265
|
self._retry_delay = retry_delay or float(os.getenv('FILE_HUB_RETRY_DELAY', '1.0'))
|
@@ -257,10 +283,13 @@ class TamarFileHubClient:
|
|
257
283
|
default_metadata=default_metadata,
|
258
284
|
user_context=user_context,
|
259
285
|
request_context=request_context,
|
286
|
+
enable_logging=enable_logging,
|
287
|
+
log_level=log_level,
|
260
288
|
)
|
261
289
|
self._blob_service = None
|
262
290
|
self._file_service = None
|
263
291
|
self._folder_service = None
|
292
|
+
self._taple_service = None
|
264
293
|
self._auto_connect = auto_connect
|
265
294
|
self._connected = False
|
266
295
|
|
@@ -293,6 +322,14 @@ class TamarFileHubClient:
|
|
293
322
|
self._folder_service = SyncFolderService(self._client)
|
294
323
|
return self._folder_service
|
295
324
|
|
325
|
+
@property
|
326
|
+
def taples(self) -> SyncTapleService:
|
327
|
+
"""获取 Taple 服务"""
|
328
|
+
self._ensure_connected()
|
329
|
+
if self._taple_service is None:
|
330
|
+
self._taple_service = SyncTapleService(self._client)
|
331
|
+
return self._taple_service
|
332
|
+
|
296
333
|
def set_user_context(self, org_id: str, user_id: str, role: Role = Role.ACCOUNT, actor_id: Optional[str] = None):
|
297
334
|
"""
|
298
335
|
设置用户上下文信息
|
@@ -13,6 +13,9 @@ from typing import Optional, Dict, List, Tuple
|
|
13
13
|
from ..enums import Role
|
14
14
|
from ..errors import ConnectionError
|
15
15
|
from ..schemas.context import UserContext, RequestContext, FullContext
|
16
|
+
from ..utils.logging import get_logger, setup_logging
|
17
|
+
from .interceptors import create_async_interceptors
|
18
|
+
import logging
|
16
19
|
|
17
20
|
|
18
21
|
class AsyncGrpcClient:
|
@@ -21,7 +24,7 @@ class AsyncGrpcClient:
|
|
21
24
|
def __init__(
|
22
25
|
self,
|
23
26
|
host: str = "localhost",
|
24
|
-
port: int =
|
27
|
+
port: Optional[int] = None,
|
25
28
|
secure: bool = False,
|
26
29
|
credentials: Optional[dict] = None,
|
27
30
|
options: Optional[list] = None,
|
@@ -30,13 +33,15 @@ class AsyncGrpcClient:
|
|
30
33
|
default_metadata: Optional[Dict[str, str]] = None,
|
31
34
|
user_context: Optional[UserContext] = None,
|
32
35
|
request_context: Optional[RequestContext] = None,
|
36
|
+
enable_logging: bool = True,
|
37
|
+
log_level: str = "INFO",
|
33
38
|
):
|
34
39
|
"""
|
35
40
|
初始化异步gRPC客户端
|
36
41
|
|
37
42
|
Args:
|
38
|
-
host:
|
39
|
-
port:
|
43
|
+
host: 服务器地址(可以是域名或IP)
|
44
|
+
port: 服务器端口(可选,如果不指定则根据secure自动选择)
|
40
45
|
secure: 是否使用安全连接(TLS)
|
41
46
|
credentials: 认证凭据字典(如 {'api_key': 'xxx'})
|
42
47
|
options: gRPC通道选项
|
@@ -45,10 +50,19 @@ class AsyncGrpcClient:
|
|
45
50
|
default_metadata: 默认的元数据(如 org_id, user_id 等)
|
46
51
|
user_context: 用户上下文
|
47
52
|
request_context: 请求上下文
|
53
|
+
enable_logging: 是否启用日志记录
|
54
|
+
log_level: 日志级别
|
48
55
|
"""
|
49
56
|
self.host = host
|
50
57
|
self.port = port
|
51
|
-
|
58
|
+
|
59
|
+
# 构建地址:如果没有指定端口,则使用域名作为地址
|
60
|
+
if port is not None:
|
61
|
+
self.address = f"{host}:{port}"
|
62
|
+
else:
|
63
|
+
# 如果没有指定端口,直接使用host
|
64
|
+
# gRPC会自动根据secure选择默认端口(80/443)
|
65
|
+
self.address = host
|
52
66
|
self.secure = secure
|
53
67
|
self.credentials = credentials
|
54
68
|
self.options = options or []
|
@@ -58,6 +72,20 @@ class AsyncGrpcClient:
|
|
58
72
|
self._channel: Optional[grpc.aio.Channel] = None
|
59
73
|
self._stubs = {}
|
60
74
|
self._stub_lock = asyncio.Lock()
|
75
|
+
|
76
|
+
# 日志配置
|
77
|
+
self.enable_logging = enable_logging
|
78
|
+
self.log_level = log_level
|
79
|
+
|
80
|
+
# 只有在明确启用日志时才设置SDK日志
|
81
|
+
# 默认不设置,让用户自己控制
|
82
|
+
if enable_logging:
|
83
|
+
# 检查是否已经有处理器,避免重复设置
|
84
|
+
sdk_logger = get_logger()
|
85
|
+
if not sdk_logger.handlers:
|
86
|
+
setup_logging(level=log_level, enable_grpc_logging=True, use_json_format=True)
|
87
|
+
|
88
|
+
self.logger = get_logger()
|
61
89
|
|
62
90
|
# 上下文管理
|
63
91
|
self._user_context = user_context
|
@@ -85,7 +113,7 @@ class AsyncGrpcClient:
|
|
85
113
|
return RequestContext(
|
86
114
|
client_ip=client_ip,
|
87
115
|
client_type="python-sdk",
|
88
|
-
client_version="1.0.0",
|
116
|
+
client_version="1.0.0",
|
89
117
|
user_agent=f"FileHubClient/1.0.0 Python/{platform.python_version()} {platform.system()}/{platform.release()}"
|
90
118
|
)
|
91
119
|
|
@@ -125,16 +153,21 @@ class AsyncGrpcClient:
|
|
125
153
|
|
126
154
|
channel_credentials = self._create_channel_credentials()
|
127
155
|
|
156
|
+
# 创建拦截器
|
157
|
+
interceptors = create_async_interceptors() if self.enable_logging else []
|
158
|
+
|
128
159
|
if channel_credentials:
|
129
160
|
self._channel = grpc.aio.secure_channel(
|
130
161
|
self.address,
|
131
162
|
channel_credentials,
|
132
|
-
options=self.options
|
163
|
+
options=self.options,
|
164
|
+
interceptors=interceptors
|
133
165
|
)
|
134
166
|
else:
|
135
167
|
self._channel = grpc.aio.insecure_channel(
|
136
168
|
self.address,
|
137
|
-
options=self.options
|
169
|
+
options=self.options,
|
170
|
+
interceptors=interceptors
|
138
171
|
)
|
139
172
|
|
140
173
|
# 连接
|
@@ -144,12 +177,41 @@ class AsyncGrpcClient:
|
|
144
177
|
raise ConnectionError(f"连接超时:{self.address}")
|
145
178
|
|
146
179
|
# 连接成功
|
180
|
+
if self.enable_logging:
|
181
|
+
log_record = logging.LogRecord(
|
182
|
+
name=self.logger.name,
|
183
|
+
level=logging.INFO,
|
184
|
+
pathname="",
|
185
|
+
lineno=0,
|
186
|
+
msg=f"🔗 已连接到 gRPC 服务器",
|
187
|
+
args=(),
|
188
|
+
exc_info=None
|
189
|
+
)
|
190
|
+
log_record.log_type = "info"
|
191
|
+
log_record.data = {"server": self.address}
|
192
|
+
self.logger.handle(log_record)
|
147
193
|
return
|
148
194
|
|
149
195
|
except Exception as e:
|
150
196
|
last_error = e
|
151
197
|
if attempt < self.retry_count - 1:
|
152
|
-
|
198
|
+
if self.enable_logging:
|
199
|
+
log_record = logging.LogRecord(
|
200
|
+
name=self.logger.name,
|
201
|
+
level=logging.WARNING,
|
202
|
+
pathname="",
|
203
|
+
lineno=0,
|
204
|
+
msg=f"⚠️ 连接失败,正在重试",
|
205
|
+
args=(),
|
206
|
+
exc_info=None
|
207
|
+
)
|
208
|
+
log_record.log_type = "info"
|
209
|
+
log_record.data = {
|
210
|
+
"attempt": attempt + 1,
|
211
|
+
"max_attempts": self.retry_count,
|
212
|
+
"error": str(e)
|
213
|
+
}
|
214
|
+
self.logger.handle(log_record)
|
153
215
|
if self._channel:
|
154
216
|
await self._channel.close()
|
155
217
|
self._channel = None
|
@@ -167,6 +229,19 @@ class AsyncGrpcClient:
|
|
167
229
|
async def close(self):
|
168
230
|
"""关闭连接"""
|
169
231
|
if self._channel:
|
232
|
+
if self.enable_logging:
|
233
|
+
log_record = logging.LogRecord(
|
234
|
+
name=self.logger.name,
|
235
|
+
level=logging.INFO,
|
236
|
+
pathname="",
|
237
|
+
lineno=0,
|
238
|
+
msg=f"👋 正在关闭 gRPC 连接",
|
239
|
+
args=(),
|
240
|
+
exc_info=None
|
241
|
+
)
|
242
|
+
log_record.log_type = "info"
|
243
|
+
log_record.data = {"server": self.address}
|
244
|
+
self.logger.handle(log_record)
|
170
245
|
await self._channel.close()
|
171
246
|
self._channel = None
|
172
247
|
self._stubs.clear()
|
@@ -190,11 +265,12 @@ class AsyncGrpcClient:
|
|
190
265
|
self._stubs[stub_name] = stub_class(self._channel)
|
191
266
|
return self._stubs[stub_name]
|
192
267
|
|
193
|
-
def build_metadata(self, **kwargs) -> List[Tuple[str, str]]:
|
268
|
+
def build_metadata(self, *, request_id: Optional[str] = None, **kwargs) -> List[Tuple[str, str]]:
|
194
269
|
"""
|
195
270
|
构建请求元数据
|
196
271
|
|
197
272
|
Args:
|
273
|
+
request_id: 显式指定的请求ID,如果提供则优先使用
|
198
274
|
**kwargs: 要覆盖或添加的元数据
|
199
275
|
|
200
276
|
Returns:
|
@@ -208,8 +284,12 @@ class AsyncGrpcClient:
|
|
208
284
|
# 添加/覆盖传入的元数据
|
209
285
|
metadata.update(kwargs)
|
210
286
|
|
211
|
-
#
|
212
|
-
if
|
287
|
+
# 处理 request_id(优先级:显式传入 > metadata中的x-request-id > RequestContext > 自动生成)
|
288
|
+
if request_id is not None:
|
289
|
+
# 优先使用显式传入的 request_id
|
290
|
+
metadata['x-request-id'] = request_id
|
291
|
+
elif 'x-request-id' not in metadata:
|
292
|
+
# 如果没有显式传入且metadata中也没有,则尝试从RequestContext获取或自动生成
|
213
293
|
metadata['x-request-id'] = (
|
214
294
|
self._request_context.extra.get("request_id") or str(uuid.uuid4())
|
215
295
|
)
|