tamar-file-hub-client 0.0.3__tar.gz → 0.0.5__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.
Files changed (63) hide show
  1. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/PKG-INFO +1 -1
  2. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/interceptors.py +35 -4
  3. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/file/async_blob_service.py +19 -9
  4. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/file/sync_blob_service.py +21 -12
  5. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/logging.py +4 -0
  6. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/retry.py +46 -4
  7. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/setup.py +1 -1
  8. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/tamar_file_hub_client.egg-info/PKG-INFO +1 -1
  9. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/MANIFEST.in +0 -0
  10. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/README.md +0 -0
  11. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/__init__.py +0 -0
  12. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/client.py +0 -0
  13. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/enums/__init__.py +0 -0
  14. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/enums/export_format.py +0 -0
  15. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/enums/role.py +0 -0
  16. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/enums/upload_mode.py +0 -0
  17. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/errors/__init__.py +0 -0
  18. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/errors/exceptions.py +0 -0
  19. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/py.typed +0 -0
  20. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/__init__.py +0 -0
  21. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/async_client.py +0 -0
  22. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/gen/__init__.py +0 -0
  23. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/gen/file_service_pb2.py +0 -0
  24. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/gen/file_service_pb2_grpc.py +0 -0
  25. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/gen/folder_service_pb2.py +0 -0
  26. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/gen/folder_service_pb2_grpc.py +0 -0
  27. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/gen/taple_service_pb2.py +0 -0
  28. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/gen/taple_service_pb2_grpc.py +0 -0
  29. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/generate_grpc.py +0 -0
  30. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/protos/file_service.proto +0 -0
  31. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/protos/folder_service.proto +0 -0
  32. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/protos/taple_service.proto +0 -0
  33. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/rpc/sync_client.py +0 -0
  34. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/schemas/__init__.py +0 -0
  35. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/schemas/context.py +0 -0
  36. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/schemas/file.py +0 -0
  37. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/schemas/folder.py +0 -0
  38. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/schemas/taple.py +0 -0
  39. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/__init__.py +0 -0
  40. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/file/__init__.py +0 -0
  41. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/file/async_file_service.py +0 -0
  42. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/file/base_file_service.py +0 -0
  43. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/file/sync_file_service.py +0 -0
  44. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/folder/__init__.py +0 -0
  45. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/folder/async_folder_service.py +0 -0
  46. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/folder/sync_folder_service.py +0 -0
  47. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/taple/__init__.py +0 -0
  48. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/taple/async_taple_service.py +0 -0
  49. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/taple/base_taple_service.py +0 -0
  50. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/taple/idempotent_taple_mixin.py +0 -0
  51. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/services/taple/sync_taple_service.py +0 -0
  52. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/__init__.py +0 -0
  53. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/converter.py +0 -0
  54. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/download_helper.py +0 -0
  55. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/file_utils.py +0 -0
  56. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/idempotency.py +0 -0
  57. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/smart_retry.py +0 -0
  58. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/file_hub_client/utils/upload_helper.py +0 -0
  59. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/setup.cfg +0 -0
  60. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/tamar_file_hub_client.egg-info/SOURCES.txt +0 -0
  61. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/tamar_file_hub_client.egg-info/dependency_links.txt +0 -0
  62. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/tamar_file_hub_client.egg-info/requires.txt +0 -0
  63. {tamar_file_hub_client-0.0.3 → tamar_file_hub_client-0.0.5}/tamar_file_hub_client.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamar-file-hub-client
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: A Python SDK for gRPC-based file management system
5
5
  Home-page: https://github.com/Tamar-Edge-AI/file-hub-client
6
6
  Author: Oscar Ou
@@ -86,8 +86,29 @@ def _sanitize_request_data(data: Any, max_string_length: int = 200, max_binary_p
86
86
  # 递归处理字典
87
87
  result = {}
88
88
  for key, value in data.items():
89
- # 检查是否是常见的二进制/内容字段
90
- if key.lower() in ['content', 'data', 'file', 'file_content', 'binary', 'blob', 'bytes', 'image', 'attachment']:
89
+ # 检查是否是需要特殊处理的字段
90
+ if key.lower() in ['operations'] and isinstance(value, list):
91
+ # 对于 operations 字段,特殊处理以显示操作类型和数量
92
+ if len(value) > 5:
93
+ ops_summary = []
94
+ # 统计操作类型
95
+ op_types = {}
96
+ for op in value:
97
+ if isinstance(op, dict):
98
+ for op_type in ['edit', 'create', 'update', 'delete', 'clear']:
99
+ if op_type in op:
100
+ op_types[op_type] = op_types.get(op_type, 0) + 1
101
+
102
+ # 显示前3个操作
103
+ for i in range(min(3, len(value))):
104
+ ops_summary.append(_sanitize_request_data(value[i], max_string_length, max_binary_preview))
105
+
106
+ # 添加统计信息
107
+ ops_summary.append(f"... 总计 {len(value)} 个操作: {', '.join(f'{k}={v}' for k, v in op_types.items())}")
108
+ result[key] = ops_summary
109
+ else:
110
+ result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
111
+ elif key.lower() in ['content', 'data', 'file', 'file_content', 'binary', 'blob', 'bytes', 'image', 'attachment']:
91
112
  if isinstance(value, (bytes, bytearray)):
92
113
  # 二进制内容,显示长度和预览
93
114
  preview = base64.b64encode(value[:max_binary_preview]).decode('utf-8')
@@ -106,8 +127,18 @@ def _sanitize_request_data(data: Any, max_string_length: int = 200, max_binary_p
106
127
  result[key] = _sanitize_request_data(value, max_string_length, max_binary_preview)
107
128
  return result
108
129
  elif isinstance(data, list):
109
- # 递归处理列表
110
- return [_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data]
130
+ # 递归处理列表,限制列表长度以避免日志过长
131
+ max_list_items = 10 # 最多显示10个元素
132
+ if len(data) > max_list_items:
133
+ # 显示前5个和后5个元素
134
+ preview_items = (
135
+ [_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data[:5]] +
136
+ [f"... {len(data) - max_list_items} more items ..."] +
137
+ [_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data[-5:]]
138
+ )
139
+ return preview_items
140
+ else:
141
+ return [_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data]
111
142
  elif isinstance(data, tuple):
112
143
  # 递归处理元组
113
144
  return tuple(_sanitize_request_data(item, max_string_length, max_binary_preview) for item in data)
@@ -401,12 +401,12 @@ class AsyncBlobService(BaseFileService):
401
401
  # 参数验证:必须提供 file 或 url 之一
402
402
  if file is None and not url:
403
403
  raise ValidationError("必须提供 file 或 url 参数之一")
404
-
404
+
405
405
  # 如果提供了URL,先下载文件
406
406
  if url:
407
407
  # 下载文件到内存
408
408
  downloaded_content = await self.http_downloader.download(url)
409
-
409
+
410
410
  # 如果没有指定文件名,从URL中提取
411
411
  if not file_name:
412
412
  from urllib.parse import urlparse
@@ -414,22 +414,32 @@ class AsyncBlobService(BaseFileService):
414
414
  parsed_url = urlparse(url)
415
415
  url_path = PathLib(parsed_url.path)
416
416
  file_name = url_path.name if url_path.name else f"download_{hashlib.md5(url.encode()).hexdigest()[:8]}"
417
-
417
+
418
418
  # 使用下载的内容作为file参数
419
419
  file = downloaded_content
420
-
420
+
421
421
  # 提取文件信息(bytes会返回默认的MIME类型,我们稍后会基于文件名重新计算)
422
- extracted_file_name, content, file_size, _, _, file_hash = self._extract_file_info(file)
423
-
422
+ _, content, file_size, _, _, file_hash = self._extract_file_info(file)
423
+
424
424
  # file_name已经在上面设置了(要么是用户指定的,要么是从URL提取的)
425
425
  extracted_file_name = file_name
426
-
426
+
427
427
  # 基于文件名计算文件类型和MIME类型
428
- file_type = Path(extracted_file_name).suffix.lstrip('.').lower() if Path(extracted_file_name).suffix else 'dat'
428
+ file_type = Path(extracted_file_name).suffix.lstrip('.').lower() if Path(
429
+ extracted_file_name).suffix else 'dat'
429
430
  mime_type = get_file_mime_type(Path(extracted_file_name))
430
431
  else:
431
432
  # 解析文件参数,提取文件信息
432
- extracted_file_name, content, file_size, mime_type, file_type, file_hash = self._extract_file_info(file)
433
+ extracted_file_name, content, file_size, extract_mime_type, extract_file_type, file_hash = self._extract_file_info(
434
+ file)
435
+ if file_name:
436
+ extracted_file_name = file_name
437
+ mime_type = get_file_mime_type(file_name)
438
+ file_type = Path(extracted_file_name).suffix.lstrip('.').lower() if Path(
439
+ extracted_file_name).suffix else 'dat'
440
+ else:
441
+ mime_type = extract_mime_type
442
+ file_type = extract_file_type
433
443
 
434
444
  # 根据文件大小自动选择上传模式
435
445
  if mode == UploadMode.NORMAL:
@@ -392,19 +392,19 @@ class SyncBlobService(BaseFileService):
392
392
 
393
393
  Returns:
394
394
  文件信息
395
-
395
+
396
396
  Note:
397
397
  必须提供 file 或 url 参数之一
398
398
  """
399
399
  # 参数验证:必须提供 file 或 url 之一
400
400
  if file is None and not url:
401
401
  raise ValidationError("必须提供 file 或 url 参数之一")
402
-
402
+
403
403
  # 如果提供了URL,先下载文件
404
404
  if url:
405
405
  # 下载文件到内存
406
406
  downloaded_content = self.http_downloader.download(url)
407
-
407
+
408
408
  # 如果没有指定文件名,从URL中提取
409
409
  if not file_name:
410
410
  from urllib.parse import urlparse
@@ -412,24 +412,32 @@ class SyncBlobService(BaseFileService):
412
412
  parsed_url = urlparse(url)
413
413
  url_path = PathLib(parsed_url.path)
414
414
  file_name = url_path.name if url_path.name else f"download_{hashlib.md5(url.encode()).hexdigest()[:8]}"
415
-
415
+
416
416
  # 使用下载的内容作为file参数
417
417
  file = downloaded_content
418
-
418
+
419
419
  # 提取文件信息(bytes会返回默认的MIME类型,我们稍后会基于文件名重新计算)
420
- extracted_file_name, content, file_size, _, _, file_hash = self._extract_file_info(file)
421
-
420
+ _, content, file_size, _, _, file_hash = self._extract_file_info(file)
421
+
422
422
  # file_name已经在上面设置了(要么是用户指定的,要么是从URL提取的)
423
423
  extracted_file_name = file_name
424
-
424
+
425
425
  # 基于文件名计算文件类型和MIME类型
426
- file_type = Path(extracted_file_name).suffix.lstrip('.').lower() if Path(extracted_file_name).suffix else 'dat'
426
+ file_type = Path(extracted_file_name).suffix.lstrip('.').lower() if Path(
427
+ extracted_file_name).suffix else 'dat'
427
428
  mime_type = get_file_mime_type(Path(extracted_file_name))
428
429
  else:
429
430
  # 解析文件参数,提取文件信息
430
- extracted_file_name, content, file_size, mime_type, file_type, file_hash = self._extract_file_info(file)
431
- if not file_name:
432
- mime_type = get_file_mime_type(extracted_file_name)
431
+ extracted_file_name, content, file_size, extract_mime_type, extract_file_type, file_hash = self._extract_file_info(
432
+ file)
433
+ if file_name:
434
+ extracted_file_name = file_name
435
+ mime_type = get_file_mime_type(file_name)
436
+ file_type = Path(extracted_file_name).suffix.lstrip('.').lower() if Path(
437
+ extracted_file_name).suffix else 'dat'
438
+ else:
439
+ mime_type = extract_mime_type
440
+ file_type = extract_file_type
433
441
 
434
442
  # 根据文件大小自动选择上传模式
435
443
  if mode == UploadMode.NORMAL:
@@ -503,6 +511,7 @@ class SyncBlobService(BaseFileService):
503
511
 
504
512
  Args:
505
513
  file_id: 文件ID
514
+ expire_seconds: 过期时间(秒)
506
515
  request_id: 请求ID(可选,如果不提供则自动生成)
507
516
  **metadata: 额外的元数据(如 x-org-id, x-user-id 等)
508
517
 
@@ -109,6 +109,10 @@ def setup_logging(
109
109
  # 防止日志传播到根日志记录器 - 保持SDK日志独立
110
110
  logger.propagate = False
111
111
 
112
+ # 对整个 file_hub_client 包设置隔离,确保所有子模块的日志都不会传播
113
+ parent_logger = logging.getLogger('file_hub_client')
114
+ parent_logger.propagate = False
115
+
112
116
  # 初始化日志(使用JSON格式)
113
117
  if enable_grpc_logging:
114
118
  log_record = logging.LogRecord(
@@ -67,10 +67,20 @@ def retry_with_backoff(
67
67
  raise
68
68
 
69
69
  if attempt < max_retries:
70
+ # 提取更详细的错误信息
71
+ error_details = str(e)
72
+ if hasattr(e, 'code') and hasattr(e, 'details'):
73
+ # gRPC 错误
74
+ error_details = f"gRPC {e.code().name}: {e.details()}"
75
+ elif hasattr(e, 'response') and hasattr(e.response, 'status_code'):
76
+ # HTTP 错误
77
+ error_details = f"HTTP {e.response.status_code}: {str(e)}"
78
+
70
79
  logger.warning(
71
80
  f"🔄 触发重试 | 操作: {func.__name__} | "
72
81
  f"尝试: {attempt + 1}/{max_retries + 1} | "
73
- f"错误: {type(e).__name__}: {str(e)} | "
82
+ f"错误类型: {type(e).__name__} | "
83
+ f"错误详情: {error_details} | "
74
84
  f"延迟: {delay:.1f}秒"
75
85
  )
76
86
  await asyncio.sleep(delay)
@@ -109,10 +119,20 @@ def retry_with_backoff(
109
119
  raise
110
120
 
111
121
  if attempt < max_retries:
122
+ # 提取更详细的错误信息
123
+ error_details = str(e)
124
+ if hasattr(e, 'code') and hasattr(e, 'details'):
125
+ # gRPC 错误
126
+ error_details = f"gRPC {e.code().name}: {e.details()}"
127
+ elif hasattr(e, 'response') and hasattr(e.response, 'status_code'):
128
+ # HTTP 错误
129
+ error_details = f"HTTP {e.response.status_code}: {str(e)}"
130
+
112
131
  logger.warning(
113
132
  f"🔄 触发重试 | 操作: {func.__name__} | "
114
133
  f"尝试: {attempt + 1}/{max_retries + 1} | "
115
- f"错误: {type(e).__name__}: {str(e)} | "
134
+ f"错误类型: {type(e).__name__} | "
135
+ f"错误详情: {error_details} | "
116
136
  f"延迟: {delay:.1f}秒"
117
137
  )
118
138
  time.sleep(delay)
@@ -162,10 +182,21 @@ def retry_on_lock_conflict(
162
182
  if _is_lock_conflict(result):
163
183
  last_result = result
164
184
  if attempt < max_retries:
185
+ # 提取冲突详细信息
186
+ conflict_details = "lock_conflict"
187
+ if isinstance(result, dict):
188
+ conflict_info = result.get('conflict_info', {})
189
+ if conflict_info:
190
+ conflict_details = f"{conflict_info.get('conflict_type', 'lock_conflict')}"
191
+ if 'resolution_suggestion' in conflict_info:
192
+ conflict_details += f" - {conflict_info['resolution_suggestion']}"
193
+ if 'error_message' in result:
194
+ conflict_details += f" - {result['error_message']}"
195
+
165
196
  logger.warning(
166
197
  f"🔒 锁冲突重试 | 操作: {func.__name__} | "
167
198
  f"尝试: {attempt + 1}/{max_retries + 1} | "
168
- f"冲突类型: lock_conflict | "
199
+ f"冲突详情: {conflict_details} | "
169
200
  f"延迟: {delay:.1f}秒"
170
201
  )
171
202
  await asyncio.sleep(delay)
@@ -196,10 +227,21 @@ def retry_on_lock_conflict(
196
227
  if _is_lock_conflict(result):
197
228
  last_result = result
198
229
  if attempt < max_retries:
230
+ # 提取冲突详细信息
231
+ conflict_details = "lock_conflict"
232
+ if isinstance(result, dict):
233
+ conflict_info = result.get('conflict_info', {})
234
+ if conflict_info:
235
+ conflict_details = f"{conflict_info.get('conflict_type', 'lock_conflict')}"
236
+ if 'resolution_suggestion' in conflict_info:
237
+ conflict_details += f" - {conflict_info['resolution_suggestion']}"
238
+ if 'error_message' in result:
239
+ conflict_details += f" - {result['error_message']}"
240
+
199
241
  logger.warning(
200
242
  f"🔒 锁冲突重试 | 操作: {func.__name__} | "
201
243
  f"尝试: {attempt + 1}/{max_retries + 1} | "
202
- f"冲突类型: lock_conflict | "
244
+ f"冲突详情: {conflict_details} | "
203
245
  f"延迟: {delay:.1f}秒"
204
246
  )
205
247
  time.sleep(delay)
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="tamar-file-hub-client",
5
- version="0.0.3",
5
+ version="0.0.5",
6
6
  description="A Python SDK for gRPC-based file management system",
7
7
  long_description=open("README.md", encoding="utf-8").read(),
8
8
  long_description_content_type="text/markdown",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tamar-file-hub-client
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: A Python SDK for gRPC-based file management system
5
5
  Home-page: https://github.com/Tamar-Edge-AI/file-hub-client
6
6
  Author: Oscar Ou