tamar-file-hub-client 0.0.1__py3-none-any.whl → 0.0.2__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.
Files changed (36) hide show
  1. file_hub_client/__init__.py +39 -0
  2. file_hub_client/client.py +43 -6
  3. file_hub_client/rpc/async_client.py +91 -11
  4. file_hub_client/rpc/gen/taple_service_pb2.py +225 -0
  5. file_hub_client/rpc/gen/taple_service_pb2_grpc.py +1626 -0
  6. file_hub_client/rpc/generate_grpc.py +2 -2
  7. file_hub_client/rpc/interceptors.py +550 -0
  8. file_hub_client/rpc/protos/taple_service.proto +874 -0
  9. file_hub_client/rpc/sync_client.py +91 -9
  10. file_hub_client/schemas/__init__.py +60 -0
  11. file_hub_client/schemas/taple.py +413 -0
  12. file_hub_client/services/__init__.py +5 -0
  13. file_hub_client/services/file/async_blob_service.py +558 -482
  14. file_hub_client/services/file/async_file_service.py +18 -9
  15. file_hub_client/services/file/base_file_service.py +19 -6
  16. file_hub_client/services/file/sync_blob_service.py +554 -478
  17. file_hub_client/services/file/sync_file_service.py +18 -9
  18. file_hub_client/services/folder/async_folder_service.py +20 -11
  19. file_hub_client/services/folder/sync_folder_service.py +20 -11
  20. file_hub_client/services/taple/__init__.py +10 -0
  21. file_hub_client/services/taple/async_taple_service.py +2281 -0
  22. file_hub_client/services/taple/base_taple_service.py +353 -0
  23. file_hub_client/services/taple/idempotent_taple_mixin.py +142 -0
  24. file_hub_client/services/taple/sync_taple_service.py +2256 -0
  25. file_hub_client/utils/__init__.py +43 -1
  26. file_hub_client/utils/file_utils.py +59 -11
  27. file_hub_client/utils/idempotency.py +196 -0
  28. file_hub_client/utils/logging.py +315 -0
  29. file_hub_client/utils/retry.py +241 -2
  30. file_hub_client/utils/smart_retry.py +403 -0
  31. tamar_file_hub_client-0.0.2.dist-info/METADATA +2050 -0
  32. tamar_file_hub_client-0.0.2.dist-info/RECORD +57 -0
  33. tamar_file_hub_client-0.0.1.dist-info/METADATA +0 -874
  34. tamar_file_hub_client-0.0.1.dist-info/RECORD +0 -44
  35. {tamar_file_hub_client-0.0.1.dist-info → tamar_file_hub_client-0.0.2.dist-info}/WHEEL +0 -0
  36. {tamar_file_hub_client-0.0.1.dist-info → tamar_file_hub_client-0.0.2.dist-info}/top_level.txt +0 -0
@@ -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: 服务器地址(默认从环境变量 FILE_HUB_HOST 读取,否则使用 localhost)
46
- port: 服务器端口(默认从环境变量 FILE_HUB_PORT 读取,否则使用 50051)
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
- self._port = port or int(os.getenv('FILE_HUB_PORT', '50051'))
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: 服务器地址(默认从环境变量 FILE_HUB_HOST 读取,否则使用 localhost)
216
- port: 服务器端口(默认从环境变量 FILE_HUB_PORT 读取,否则使用 50051)
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
- self._port = port or int(os.getenv('FILE_HUB_PORT', '50051'))
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 = 50051,
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
- self.address = f"{host}:{port}"
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", # TODO: 从包版本获取
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
- print(f"连接失败 (尝试 {attempt + 1}/{self.retry_count}): {str(e)}")
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
- # 如果没有 request_id,自动生成一个
212
- if 'x-request-id' not in metadata:
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
  )