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.
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 +556 -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.3.dist-info/METADATA +2050 -0
  32. tamar_file_hub_client-0.0.3.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.3.dist-info}/WHEEL +0 -0
  36. {tamar_file_hub_client-0.0.1.dist-info → tamar_file_hub_client-0.0.3.dist-info}/top_level.txt +0 -0
@@ -27,7 +27,7 @@ def generate_grpc_code():
27
27
 
28
28
  # 生成gRPC代码
29
29
  cmd = [
30
- "python", "-m", "grpc_tools.protoc",
30
+ "python3", "-m", "grpc_tools.protoc",
31
31
  f"-I{proto_dir}",
32
32
  f"--python_out={gen_dir}",
33
33
  f"--grpc_python_out={gen_dir}",
@@ -55,7 +55,7 @@ def fix_imports(gen_dir):
55
55
  content = f.read()
56
56
 
57
57
  # 修复导入路径 - 现在有多个proto文件
58
- for proto_name in ["file_service", "folder_service"]:
58
+ for proto_name in ["file_service", "folder_service", "taple_service"]:
59
59
  content = content.replace(f"import {proto_name}_pb2", f"from . import {proto_name}_pb2")
60
60
 
61
61
  with open(py_file, "w", encoding="utf-8") as f:
@@ -0,0 +1,550 @@
1
+ """
2
+ gRPC拦截器,用于自动记录请求和响应日志
3
+ """
4
+ import time
5
+ import asyncio
6
+ from typing import Any, Callable, Optional, Dict, List, Tuple, Union
7
+ import grpc
8
+ from grpc import aio
9
+ import base64
10
+ import re
11
+
12
+ from ..utils.logging import get_logger, GrpcRequestLogger
13
+
14
+
15
+ def _extract_method_name(method: str) -> str:
16
+ """从gRPC方法路径中提取方法名"""
17
+ # 方法格式: /package.service/MethodName
18
+ parts = method.split('/')
19
+ if len(parts) >= 2:
20
+ return parts[-1]
21
+ return method
22
+
23
+ def _extract_request_id(metadata: List[Tuple[str, str]]) -> str:
24
+ """从元数据中提取请求ID"""
25
+ if not metadata:
26
+ return "unknown"
27
+
28
+ try:
29
+ metadata_dict = dict(metadata)
30
+ return metadata_dict.get('x-request-id', 'unknown')
31
+ except Exception:
32
+ # 如果元数据解析失败,返回unknown
33
+ return "unknown"
34
+
35
+ def _metadata_to_dict(metadata: List[Tuple[str, str]]) -> Dict[str, str]:
36
+ """将元数据转换为字典"""
37
+ try:
38
+ return dict(metadata) if metadata else {}
39
+ except Exception:
40
+ # 如果元数据解析失败,返回空字典
41
+ return {}
42
+
43
+
44
+ def _is_base64_string(value: str, min_length: int = 100) -> bool:
45
+ """检查字符串是否可能是base64编码的内容"""
46
+ if not isinstance(value, str) or len(value) < min_length:
47
+ return False
48
+
49
+ # 基本的base64格式检查
50
+ base64_pattern = re.compile(r'^[A-Za-z0-9+/]*={0,2}$')
51
+ if not base64_pattern.match(value):
52
+ return False
53
+
54
+ # 尝试解码以验证
55
+ try:
56
+ decoded = base64.b64decode(value, validate=True)
57
+ # 如果能成功解码且长度超过阈值,认为是base64
58
+ return len(decoded) > 50
59
+ except:
60
+ return False
61
+
62
+
63
+ def _truncate_long_string(value: str, max_length: int = 200, placeholder: str = "...") -> str:
64
+ """截断长字符串,保留开头和结尾部分"""
65
+ if len(value) <= max_length:
66
+ return value
67
+
68
+ # 计算保留的开头和结尾长度
69
+ keep_length = (max_length - len(placeholder)) // 2
70
+ return f"{value[:keep_length]}{placeholder}{value[-keep_length:]}"
71
+
72
+
73
+ def _sanitize_request_data(data: Any, max_string_length: int = 200, max_binary_preview: int = 50) -> Any:
74
+ """
75
+ 清理请求数据,截断长字符串和二进制内容
76
+
77
+ Args:
78
+ data: 要清理的数据
79
+ max_string_length: 字符串的最大长度
80
+ max_binary_preview: 二进制内容预览的最大长度
81
+
82
+ Returns:
83
+ 清理后的数据
84
+ """
85
+ if isinstance(data, dict):
86
+ # 递归处理字典
87
+ result = {}
88
+ for key, value in data.items():
89
+ # 检查是否是常见的二进制/内容字段
90
+ if key.lower() in ['content', 'data', 'file', 'file_content', 'binary', 'blob', 'bytes', 'image', 'attachment']:
91
+ if isinstance(value, (bytes, bytearray)):
92
+ # 二进制内容,显示长度和预览
93
+ preview = base64.b64encode(value[:max_binary_preview]).decode('utf-8')
94
+ result[key] = f"<binary {len(value)} bytes, preview: {preview}...>"
95
+ elif isinstance(value, str):
96
+ # 检查是否是base64字符串
97
+ if _is_base64_string(value):
98
+ result[key] = f"<base64 string, length: {len(value)}, preview: {value[:max_binary_preview]}...>"
99
+ elif len(value) > max_string_length:
100
+ result[key] = _truncate_long_string(value, max_string_length)
101
+ else:
102
+ result[key] = value
103
+ else:
104
+ result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
105
+ else:
106
+ result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
107
+ return result
108
+ elif isinstance(data, list):
109
+ # 递归处理列表
110
+ return [_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data]
111
+ elif isinstance(data, tuple):
112
+ # 递归处理元组
113
+ return tuple(_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data)
114
+ elif isinstance(data, (bytes, bytearray)):
115
+ # 二进制内容
116
+ preview = base64.b64encode(data[:max_binary_preview]).decode('utf-8')
117
+ return f"<binary {len(data)} bytes, preview: {preview}...>"
118
+ elif isinstance(data, str):
119
+ # 字符串内容
120
+ if _is_base64_string(data):
121
+ return f"<base64 string, length: {len(data)}, preview: {data[:max_binary_preview]}...>"
122
+ elif len(data) > max_string_length:
123
+ return _truncate_long_string(data, max_string_length)
124
+ else:
125
+ return data
126
+ else:
127
+ # 其他类型直接返回
128
+ return data
129
+
130
+
131
+ class LoggingInterceptor:
132
+ """gRPC日志拦截器基类"""
133
+
134
+ def __init__(self):
135
+ self.logger = get_logger()
136
+ self.request_logger = GrpcRequestLogger(self.logger)
137
+
138
+
139
+ class AsyncUnaryUnaryLoggingInterceptor(LoggingInterceptor, aio.UnaryUnaryClientInterceptor):
140
+ """异步一元-一元gRPC日志拦截器"""
141
+
142
+ async def intercept_unary_unary(
143
+ self,
144
+ continuation: Callable,
145
+ client_call_details: grpc.aio.ClientCallDetails,
146
+ request: Any
147
+ ) -> Any:
148
+ """拦截一元-一元调用"""
149
+ # 安全提取方法名
150
+ try:
151
+ method = client_call_details.method
152
+ if isinstance(method, bytes):
153
+ method = method.decode('utf-8', errors='ignore')
154
+ method_name = method.split('/')[-1] if '/' in method else method
155
+ except:
156
+ method_name = "unknown"
157
+
158
+ # 安全提取request_id
159
+ request_id = "unknown"
160
+ try:
161
+ if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
162
+ for key, value in client_call_details.metadata:
163
+ if key == 'x-request-id':
164
+ request_id = value
165
+ break
166
+ except:
167
+ pass
168
+
169
+ start_time = time.time()
170
+
171
+ # 记录请求开始
172
+ if self.logger.handlers:
173
+ try:
174
+ # 安全地提取请求数据
175
+ request_data = None
176
+ try:
177
+ if hasattr(request, '__class__'):
178
+ # 将protobuf消息转换为字典
179
+ from google.protobuf.json_format import MessageToDict
180
+ request_data = MessageToDict(request, preserving_proto_field_name=True)
181
+ # 清理请求数据,截断长内容
182
+ request_data = _sanitize_request_data(request_data)
183
+ except:
184
+ # 如果转换失败,尝试其他方法
185
+ try:
186
+ request_data = str(request)
187
+ if len(request_data) > 200:
188
+ request_data = _truncate_long_string(request_data)
189
+ except:
190
+ request_data = "<无法序列化>"
191
+
192
+ self.request_logger.log_request_start(
193
+ method_name, request_id, {}, request_data
194
+ )
195
+ except:
196
+ pass
197
+
198
+ try:
199
+ # 执行实际的gRPC调用
200
+ response = await continuation(client_call_details, request)
201
+
202
+ # 记录成功
203
+ duration_ms = (time.time() - start_time) * 1000
204
+ if self.logger.handlers:
205
+ try:
206
+ self.request_logger.log_request_end(
207
+ method_name, request_id, duration_ms, None
208
+ )
209
+ except:
210
+ pass
211
+
212
+ return response
213
+
214
+ except Exception as e:
215
+ # 记录错误
216
+ duration_ms = (time.time() - start_time) * 1000
217
+ if self.logger.handlers:
218
+ try:
219
+ self.request_logger.log_request_end(
220
+ method_name, request_id, duration_ms, error=e
221
+ )
222
+ except:
223
+ pass
224
+ raise
225
+
226
+
227
+ class AsyncUnaryStreamLoggingInterceptor(LoggingInterceptor, aio.UnaryStreamClientInterceptor):
228
+ """异步一元-流gRPC日志拦截器"""
229
+
230
+ async def intercept_unary_stream(
231
+ self,
232
+ continuation: Callable,
233
+ client_call_details: grpc.aio.ClientCallDetails,
234
+ request: Any
235
+ ) -> Any:
236
+ """拦截一元-流调用"""
237
+ # 安全提取方法名
238
+ try:
239
+ method = client_call_details.method
240
+ if isinstance(method, bytes):
241
+ method = method.decode('utf-8', errors='ignore')
242
+ method_name = method.split('/')[-1] if '/' in method else method
243
+ except:
244
+ method_name = "unknown"
245
+
246
+ # 安全提取request_id
247
+ request_id = "unknown"
248
+ try:
249
+ if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
250
+ for key, value in client_call_details.metadata:
251
+ if key == 'x-request-id':
252
+ request_id = value
253
+ break
254
+ except:
255
+ pass
256
+
257
+ start_time = time.time()
258
+
259
+ # 记录请求开始
260
+ if self.logger.handlers:
261
+ try:
262
+ # 安全地提取请求数据
263
+ request_data = None
264
+ try:
265
+ if hasattr(request, '__class__'):
266
+ from google.protobuf.json_format import MessageToDict
267
+ request_data = MessageToDict(request, preserving_proto_field_name=True)
268
+ # 清理请求数据,截断长内容
269
+ request_data = _sanitize_request_data(request_data)
270
+ except:
271
+ try:
272
+ request_data = str(request)
273
+ if len(request_data) > 200:
274
+ request_data = _truncate_long_string(request_data)
275
+ except:
276
+ request_data = "<无法序列化>"
277
+
278
+ self.request_logger.log_request_start(
279
+ method_name, request_id, {}, request_data
280
+ )
281
+ except:
282
+ pass
283
+
284
+ try:
285
+ # 执行实际的gRPC调用
286
+ response_stream = await continuation(client_call_details, request)
287
+
288
+ # 记录流开始
289
+ duration_ms = (time.time() - start_time) * 1000
290
+ if self.logger.handlers:
291
+ try:
292
+ self.request_logger.log_request_end(
293
+ method_name, request_id, duration_ms, "<Stream started>"
294
+ )
295
+ except:
296
+ pass
297
+
298
+ return response_stream
299
+
300
+ except Exception as e:
301
+ # 记录请求错误
302
+ duration_ms = (time.time() - start_time) * 1000
303
+ if self.logger.handlers:
304
+ try:
305
+ self.request_logger.log_request_end(
306
+ method_name, request_id, duration_ms, error=e
307
+ )
308
+ except:
309
+ pass
310
+ raise
311
+
312
+
313
+ class AsyncStreamUnaryLoggingInterceptor(LoggingInterceptor, aio.StreamUnaryClientInterceptor):
314
+ """异步流-一元gRPC日志拦截器"""
315
+
316
+ async def intercept_stream_unary(
317
+ self,
318
+ continuation: Callable,
319
+ client_call_details: grpc.aio.ClientCallDetails,
320
+ request_iterator: Any
321
+ ) -> Any:
322
+ """拦截流-一元调用"""
323
+ # 安全提取方法名
324
+ try:
325
+ method = client_call_details.method
326
+ if isinstance(method, bytes):
327
+ method = method.decode('utf-8', errors='ignore')
328
+ method_name = method.split('/')[-1] if '/' in method else method
329
+ except:
330
+ method_name = "unknown"
331
+
332
+ # 安全提取request_id
333
+ request_id = "unknown"
334
+ try:
335
+ if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
336
+ for key, value in client_call_details.metadata:
337
+ if key == 'x-request-id':
338
+ request_id = value
339
+ break
340
+ except:
341
+ pass
342
+
343
+ start_time = time.time()
344
+
345
+ # 记录请求开始
346
+ if self.logger.handlers:
347
+ try:
348
+ self.request_logger.log_request_start(
349
+ method_name, request_id, {}, "<Stream request>"
350
+ )
351
+ except:
352
+ pass
353
+
354
+ try:
355
+ # 执行实际的gRPC调用
356
+ response = await continuation(client_call_details, request_iterator)
357
+
358
+ # 记录请求成功结束
359
+ duration_ms = (time.time() - start_time) * 1000
360
+ if self.logger.handlers:
361
+ try:
362
+ self.request_logger.log_request_end(
363
+ method_name, request_id, duration_ms, None
364
+ )
365
+ except:
366
+ pass
367
+
368
+ return response
369
+
370
+ except Exception as e:
371
+ # 记录请求错误
372
+ duration_ms = (time.time() - start_time) * 1000
373
+ if self.logger.handlers:
374
+ try:
375
+ self.request_logger.log_request_end(
376
+ method_name, request_id, duration_ms, error=e
377
+ )
378
+ except:
379
+ pass
380
+ raise
381
+
382
+
383
+ class AsyncStreamStreamLoggingInterceptor(LoggingInterceptor, aio.StreamStreamClientInterceptor):
384
+ """异步流-流gRPC日志拦截器"""
385
+
386
+ async def intercept_stream_stream(
387
+ self,
388
+ continuation: Callable,
389
+ client_call_details: grpc.aio.ClientCallDetails,
390
+ request_iterator: Any
391
+ ) -> Any:
392
+ """拦截流-流调用"""
393
+ # 安全提取方法名
394
+ try:
395
+ method = client_call_details.method
396
+ if isinstance(method, bytes):
397
+ method = method.decode('utf-8', errors='ignore')
398
+ method_name = method.split('/')[-1] if '/' in method else method
399
+ except:
400
+ method_name = "unknown"
401
+
402
+ # 安全提取request_id
403
+ request_id = "unknown"
404
+ try:
405
+ if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
406
+ for key, value in client_call_details.metadata:
407
+ if key == 'x-request-id':
408
+ request_id = value
409
+ break
410
+ except:
411
+ pass
412
+
413
+ start_time = time.time()
414
+
415
+ # 记录请求开始
416
+ if self.logger.handlers:
417
+ try:
418
+ self.request_logger.log_request_start(
419
+ method_name, request_id, {}, "<Stream request>"
420
+ )
421
+ except:
422
+ pass
423
+
424
+ try:
425
+ # 执行实际的gRPC调用
426
+ response_stream = await continuation(client_call_details, request_iterator)
427
+
428
+ # 记录流开始
429
+ duration_ms = (time.time() - start_time) * 1000
430
+ if self.logger.handlers:
431
+ try:
432
+ self.request_logger.log_request_end(
433
+ method_name, request_id, duration_ms, "<Stream started>"
434
+ )
435
+ except:
436
+ pass
437
+
438
+ return response_stream
439
+
440
+ except Exception as e:
441
+ # 记录请求错误
442
+ duration_ms = (time.time() - start_time) * 1000
443
+ if self.logger.handlers:
444
+ try:
445
+ self.request_logger.log_request_end(
446
+ method_name, request_id, duration_ms, error=e
447
+ )
448
+ except:
449
+ pass
450
+ raise
451
+
452
+
453
+ class SyncUnaryUnaryLoggingInterceptor(grpc.UnaryUnaryClientInterceptor):
454
+ """同步一元-一元gRPC日志拦截器"""
455
+
456
+ def intercept_unary_unary(self, continuation, client_call_details, request):
457
+ logger = get_logger()
458
+ request_logger = GrpcRequestLogger(logger)
459
+
460
+ # 安全提取方法名
461
+ try:
462
+ method = client_call_details.method
463
+ if isinstance(method, bytes):
464
+ method = method.decode('utf-8', errors='ignore')
465
+ method_name = method.split('/')[-1] if '/' in method else method
466
+ except:
467
+ method_name = "unknown"
468
+
469
+ # 安全提取request_id
470
+ request_id = "unknown"
471
+ try:
472
+ if hasattr(client_call_details, 'metadata') and client_call_details.metadata:
473
+ for key, value in client_call_details.metadata:
474
+ if key == 'x-request-id':
475
+ request_id = value
476
+ break
477
+ except:
478
+ pass
479
+
480
+ start_time = time.time()
481
+
482
+ # 记录请求开始
483
+ if logger.handlers:
484
+ try:
485
+ # 安全地提取请求数据
486
+ request_data = None
487
+ try:
488
+ if hasattr(request, '__class__'):
489
+ from google.protobuf.json_format import MessageToDict
490
+ request_data = MessageToDict(request, preserving_proto_field_name=True)
491
+ # 清理请求数据,截断长内容
492
+ request_data = _sanitize_request_data(request_data)
493
+ except:
494
+ try:
495
+ request_data = str(request)
496
+ if len(request_data) > 200:
497
+ request_data = _truncate_long_string(request_data)
498
+ except:
499
+ request_data = "<无法序列化>"
500
+
501
+ request_logger.log_request_start(
502
+ method_name, request_id, {}, request_data
503
+ )
504
+ except:
505
+ pass
506
+
507
+ try:
508
+ # 执行实际的gRPC调用
509
+ response = continuation(client_call_details, request)
510
+
511
+ # 记录请求成功结束
512
+ duration_ms = (time.time() - start_time) * 1000
513
+ if logger.handlers:
514
+ try:
515
+ request_logger.log_request_end(
516
+ method_name, request_id, duration_ms, None
517
+ )
518
+ except:
519
+ pass
520
+
521
+ return response
522
+
523
+ except Exception as e:
524
+ # 记录请求错误
525
+ duration_ms = (time.time() - start_time) * 1000
526
+ if logger.handlers:
527
+ try:
528
+ request_logger.log_request_end(
529
+ method_name, request_id, duration_ms, error=e
530
+ )
531
+ except:
532
+ pass
533
+ raise
534
+
535
+
536
+ def create_async_interceptors() -> List[aio.ClientInterceptor]:
537
+ """创建异步gRPC拦截器列表"""
538
+ return [
539
+ AsyncUnaryUnaryLoggingInterceptor(),
540
+ AsyncUnaryStreamLoggingInterceptor(),
541
+ AsyncStreamUnaryLoggingInterceptor(),
542
+ AsyncStreamStreamLoggingInterceptor(),
543
+ ]
544
+
545
+
546
+ def create_sync_interceptors() -> List:
547
+ """创建同步gRPC拦截器列表"""
548
+ return [
549
+ SyncUnaryUnaryLoggingInterceptor(),
550
+ ]