tamar-file-hub-client 0.0.1__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 +88 -0
- file_hub_client/client.py +414 -0
- file_hub_client/enums/__init__.py +12 -0
- file_hub_client/enums/export_format.py +16 -0
- file_hub_client/enums/role.py +7 -0
- file_hub_client/enums/upload_mode.py +11 -0
- file_hub_client/errors/__init__.py +30 -0
- file_hub_client/errors/exceptions.py +93 -0
- file_hub_client/py.typed +1 -0
- file_hub_client/rpc/__init__.py +10 -0
- file_hub_client/rpc/async_client.py +312 -0
- file_hub_client/rpc/gen/__init__.py +1 -0
- file_hub_client/rpc/gen/file_service_pb2.py +74 -0
- file_hub_client/rpc/gen/file_service_pb2_grpc.py +533 -0
- file_hub_client/rpc/gen/folder_service_pb2.py +53 -0
- file_hub_client/rpc/gen/folder_service_pb2_grpc.py +269 -0
- file_hub_client/rpc/generate_grpc.py +76 -0
- file_hub_client/rpc/protos/file_service.proto +147 -0
- file_hub_client/rpc/protos/folder_service.proto +65 -0
- file_hub_client/rpc/sync_client.py +313 -0
- file_hub_client/schemas/__init__.py +43 -0
- file_hub_client/schemas/context.py +160 -0
- file_hub_client/schemas/file.py +89 -0
- file_hub_client/schemas/folder.py +29 -0
- file_hub_client/services/__init__.py +17 -0
- file_hub_client/services/file/__init__.py +14 -0
- file_hub_client/services/file/async_blob_service.py +482 -0
- file_hub_client/services/file/async_file_service.py +257 -0
- file_hub_client/services/file/base_file_service.py +103 -0
- file_hub_client/services/file/sync_blob_service.py +478 -0
- file_hub_client/services/file/sync_file_service.py +255 -0
- file_hub_client/services/folder/__init__.py +10 -0
- file_hub_client/services/folder/async_folder_service.py +206 -0
- file_hub_client/services/folder/sync_folder_service.py +205 -0
- file_hub_client/utils/__init__.py +48 -0
- file_hub_client/utils/converter.py +108 -0
- file_hub_client/utils/download_helper.py +355 -0
- file_hub_client/utils/file_utils.py +105 -0
- file_hub_client/utils/retry.py +69 -0
- file_hub_client/utils/upload_helper.py +527 -0
- tamar_file_hub_client-0.0.1.dist-info/METADATA +874 -0
- tamar_file_hub_client-0.0.1.dist-info/RECORD +44 -0
- tamar_file_hub_client-0.0.1.dist-info/WHEEL +5 -0
- tamar_file_hub_client-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,313 @@
|
|
1
|
+
"""
|
2
|
+
同步gRPC客户端
|
3
|
+
"""
|
4
|
+
import threading
|
5
|
+
from enum import Enum
|
6
|
+
|
7
|
+
import grpc
|
8
|
+
import time
|
9
|
+
import uuid
|
10
|
+
import platform
|
11
|
+
import socket
|
12
|
+
from typing import Optional, Dict, List, Tuple
|
13
|
+
|
14
|
+
from ..enums import Role
|
15
|
+
from ..errors import ConnectionError
|
16
|
+
from ..schemas.context import UserContext, RequestContext, FullContext
|
17
|
+
|
18
|
+
|
19
|
+
class SyncGrpcClient:
|
20
|
+
"""同步gRPC客户端基类"""
|
21
|
+
|
22
|
+
def __init__(
|
23
|
+
self,
|
24
|
+
host: str = "localhost",
|
25
|
+
port: int = 50051,
|
26
|
+
secure: bool = False,
|
27
|
+
credentials: Optional[dict] = None,
|
28
|
+
options: Optional[list] = None,
|
29
|
+
retry_count: int = 3,
|
30
|
+
retry_delay: float = 1.0,
|
31
|
+
default_metadata: Optional[Dict[str, str]] = None,
|
32
|
+
user_context: Optional[UserContext] = None,
|
33
|
+
request_context: Optional[RequestContext] = None,
|
34
|
+
):
|
35
|
+
"""
|
36
|
+
初始化同步gRPC客户端
|
37
|
+
|
38
|
+
Args:
|
39
|
+
host: 服务器地址
|
40
|
+
port: 服务器端口
|
41
|
+
secure: 是否使用安全连接(TLS)
|
42
|
+
credentials: 认证凭据字典(如 {'api_key': 'xxx'})
|
43
|
+
options: gRPC通道选项
|
44
|
+
retry_count: 连接重试次数
|
45
|
+
retry_delay: 重试延迟(秒)
|
46
|
+
default_metadata: 默认的元数据(如 org_id, user_id 等)
|
47
|
+
user_context: 用户上下文
|
48
|
+
request_context: 请求上下文
|
49
|
+
"""
|
50
|
+
self.host = host
|
51
|
+
self.port = port
|
52
|
+
self.address = f"{host}:{port}"
|
53
|
+
self.secure = secure
|
54
|
+
self.credentials = credentials
|
55
|
+
self.options = options or []
|
56
|
+
self.retry_count = retry_count
|
57
|
+
self.retry_delay = retry_delay
|
58
|
+
self.default_metadata = default_metadata or {}
|
59
|
+
self._channel: Optional[grpc.Channel] = None
|
60
|
+
self._stubs = {}
|
61
|
+
self._stub_lock = threading.Lock()
|
62
|
+
|
63
|
+
# 上下文管理
|
64
|
+
self._user_context = user_context
|
65
|
+
self._request_context = request_context or self._create_default_request_context()
|
66
|
+
|
67
|
+
# 如果提供了user_context,将其添加到default_metadata
|
68
|
+
if self._user_context:
|
69
|
+
self.default_metadata.update(self._user_context.to_metadata())
|
70
|
+
|
71
|
+
# 添加请求上下文到default_metadata
|
72
|
+
self.default_metadata.update(self._request_context.to_metadata())
|
73
|
+
|
74
|
+
def _create_default_request_context(self) -> RequestContext:
|
75
|
+
"""创建默认的请求上下文"""
|
76
|
+
# 尝试获取客户端IP
|
77
|
+
client_ip = None
|
78
|
+
try:
|
79
|
+
# 获取本机IP(适用于内网环境)
|
80
|
+
hostname = socket.gethostname()
|
81
|
+
client_ip = socket.gethostbyname(hostname)
|
82
|
+
except:
|
83
|
+
pass
|
84
|
+
|
85
|
+
# 获取客户端信息
|
86
|
+
return RequestContext(
|
87
|
+
client_ip=client_ip,
|
88
|
+
client_type="python-sdk",
|
89
|
+
client_version="1.0.0", # TODO: 从包版本获取
|
90
|
+
user_agent=f"FileHubClient/1.0.0 Python/{platform.python_version()} {platform.system()}/{platform.release()}"
|
91
|
+
)
|
92
|
+
|
93
|
+
def _create_channel_credentials(self) -> Optional[grpc.ChannelCredentials]:
|
94
|
+
"""创建通道凭据"""
|
95
|
+
if not self.secure:
|
96
|
+
return None
|
97
|
+
|
98
|
+
# 使用默认的SSL凭据
|
99
|
+
channel_credentials = grpc.ssl_channel_credentials()
|
100
|
+
|
101
|
+
# 如果有API密钥,创建组合凭据
|
102
|
+
if self.credentials and 'api_key' in self.credentials:
|
103
|
+
# 创建元数据凭据
|
104
|
+
def metadata_callback(context, callback):
|
105
|
+
metadata = [('authorization', f"Bearer {self.credentials['api_key']}")]
|
106
|
+
callback(metadata, None)
|
107
|
+
|
108
|
+
call_credentials = grpc.metadata_call_credentials(metadata_callback)
|
109
|
+
channel_credentials = grpc.composite_channel_credentials(
|
110
|
+
channel_credentials,
|
111
|
+
call_credentials
|
112
|
+
)
|
113
|
+
|
114
|
+
return channel_credentials
|
115
|
+
|
116
|
+
def connect(self):
|
117
|
+
"""连接到gRPC服务器(带重试)"""
|
118
|
+
if self._channel is not None:
|
119
|
+
return
|
120
|
+
|
121
|
+
last_error = None
|
122
|
+
for attempt in range(self.retry_count):
|
123
|
+
try:
|
124
|
+
if attempt > 0:
|
125
|
+
time.sleep(self.retry_delay)
|
126
|
+
|
127
|
+
channel_credentials = self._create_channel_credentials()
|
128
|
+
|
129
|
+
if channel_credentials:
|
130
|
+
self._channel = grpc.secure_channel(
|
131
|
+
self.address,
|
132
|
+
channel_credentials,
|
133
|
+
options=self.options
|
134
|
+
)
|
135
|
+
else:
|
136
|
+
self._channel = grpc.insecure_channel(
|
137
|
+
self.address,
|
138
|
+
options=self.options
|
139
|
+
)
|
140
|
+
|
141
|
+
# 连接
|
142
|
+
try:
|
143
|
+
grpc.channel_ready_future(self._channel).result(timeout=5.0)
|
144
|
+
except grpc.FutureTimeoutError:
|
145
|
+
raise ConnectionError(f"连接超时:{self.address}")
|
146
|
+
|
147
|
+
# 连接成功
|
148
|
+
return
|
149
|
+
|
150
|
+
except Exception as e:
|
151
|
+
last_error = e
|
152
|
+
if attempt < self.retry_count - 1:
|
153
|
+
print(f"连接失败 (尝试 {attempt + 1}/{self.retry_count}): {str(e)}")
|
154
|
+
if self._channel:
|
155
|
+
self._channel.close()
|
156
|
+
self._channel = None
|
157
|
+
else:
|
158
|
+
# 最后一次尝试失败
|
159
|
+
if self._channel:
|
160
|
+
self._channel.close()
|
161
|
+
self._channel = None
|
162
|
+
|
163
|
+
# 所有重试都失败
|
164
|
+
raise ConnectionError(
|
165
|
+
f"无法连接到gRPC服务器 {self.address} (尝试了 {self.retry_count} 次): {str(last_error)}"
|
166
|
+
)
|
167
|
+
|
168
|
+
def close(self):
|
169
|
+
"""关闭连接"""
|
170
|
+
if self._channel:
|
171
|
+
self._channel.close()
|
172
|
+
self._channel = None
|
173
|
+
self._stubs.clear()
|
174
|
+
|
175
|
+
def get_stub(self, stub_class):
|
176
|
+
"""
|
177
|
+
获取gRPC stub实例
|
178
|
+
|
179
|
+
Args:
|
180
|
+
stub_class: Stub类
|
181
|
+
|
182
|
+
Returns:
|
183
|
+
Stub实例
|
184
|
+
"""
|
185
|
+
if not self._channel:
|
186
|
+
raise ConnectionError("未连接到gRPC服务器")
|
187
|
+
|
188
|
+
stub_name = stub_class.__name__
|
189
|
+
with self._stub_lock:
|
190
|
+
if stub_name not in self._stubs:
|
191
|
+
self._stubs[stub_name] = stub_class(self._channel)
|
192
|
+
return self._stubs[stub_name]
|
193
|
+
|
194
|
+
def build_metadata(self, **kwargs) -> List[Tuple[str, str]]:
|
195
|
+
"""
|
196
|
+
构建请求元数据
|
197
|
+
|
198
|
+
Args:
|
199
|
+
**kwargs: 要覆盖或添加的元数据
|
200
|
+
|
201
|
+
Returns:
|
202
|
+
元数据列表
|
203
|
+
"""
|
204
|
+
metadata = {}
|
205
|
+
|
206
|
+
# 添加默认元数据
|
207
|
+
metadata.update(self.default_metadata)
|
208
|
+
|
209
|
+
# 添加/覆盖传入的元数据
|
210
|
+
metadata.update(kwargs)
|
211
|
+
|
212
|
+
# 如果没有 request_id,自动生成一个
|
213
|
+
if 'x-request-id' not in metadata:
|
214
|
+
metadata['x-request-id'] = (
|
215
|
+
self._request_context.extra.get("request_id") or str(uuid.uuid4())
|
216
|
+
)
|
217
|
+
|
218
|
+
# 转换为 gRPC 需要的格式
|
219
|
+
result = []
|
220
|
+
for k, v in metadata.items():
|
221
|
+
if v is None:
|
222
|
+
continue
|
223
|
+
if isinstance(v, Enum):
|
224
|
+
v = v.value
|
225
|
+
result.append((k, str(v)))
|
226
|
+
|
227
|
+
return result
|
228
|
+
|
229
|
+
def update_default_metadata(self, **kwargs):
|
230
|
+
"""
|
231
|
+
更新默认元数据
|
232
|
+
|
233
|
+
Args:
|
234
|
+
**kwargs: 要更新的元数据键值对
|
235
|
+
"""
|
236
|
+
self.default_metadata.update(kwargs)
|
237
|
+
|
238
|
+
def set_user_context(self, org_id: str, user_id: str, role: Role = Role.ACCOUNT, actor_id: Optional[str] = None):
|
239
|
+
"""
|
240
|
+
设置用户上下文信息
|
241
|
+
|
242
|
+
Args:
|
243
|
+
org_id: 组织ID
|
244
|
+
user_id: 用户ID
|
245
|
+
role: 用户角色(默认为 ACCOUNT)
|
246
|
+
actor_id: 操作者ID(如果不同于 user_id)
|
247
|
+
"""
|
248
|
+
self._user_context = UserContext(
|
249
|
+
org_id=org_id,
|
250
|
+
user_id=user_id,
|
251
|
+
role=role,
|
252
|
+
actor_id=actor_id
|
253
|
+
)
|
254
|
+
# 更新到默认元数据
|
255
|
+
self.update_default_metadata(**self._user_context.to_metadata())
|
256
|
+
|
257
|
+
def get_user_context(self) -> Optional[UserContext]:
|
258
|
+
"""获取当前用户上下文"""
|
259
|
+
return self._user_context
|
260
|
+
|
261
|
+
def clear_user_context(self):
|
262
|
+
"""清除用户上下文信息"""
|
263
|
+
self._user_context = None
|
264
|
+
for key in ['x-org-id', 'x-user-id', 'x-role', 'x-actor-id']:
|
265
|
+
self.default_metadata.pop(key, None)
|
266
|
+
|
267
|
+
def set_request_context(self, request_context: RequestContext):
|
268
|
+
"""设置请求上下文"""
|
269
|
+
self._request_context = request_context
|
270
|
+
# 更新到默认元数据
|
271
|
+
self.update_default_metadata(**self._request_context.to_metadata())
|
272
|
+
|
273
|
+
def get_request_context(self) -> RequestContext:
|
274
|
+
"""获取当前请求上下文"""
|
275
|
+
return self._request_context
|
276
|
+
|
277
|
+
def update_request_context(self, **kwargs):
|
278
|
+
"""
|
279
|
+
更新请求上下文的部分字段
|
280
|
+
|
281
|
+
Args:
|
282
|
+
**kwargs: 要更新的字段
|
283
|
+
"""
|
284
|
+
if kwargs.get('client_ip'):
|
285
|
+
self._request_context.client_ip = kwargs['client_ip']
|
286
|
+
if kwargs.get('client_version'):
|
287
|
+
self._request_context.client_version = kwargs['client_version']
|
288
|
+
if kwargs.get('client_type'):
|
289
|
+
self._request_context.client_type = kwargs['client_type']
|
290
|
+
if kwargs.get('user_agent'):
|
291
|
+
self._request_context.user_agent = kwargs['user_agent']
|
292
|
+
|
293
|
+
# 处理extra字段
|
294
|
+
extra = kwargs.get('extra')
|
295
|
+
if extra and isinstance(extra, dict):
|
296
|
+
self._request_context.extra.update(extra)
|
297
|
+
|
298
|
+
# 更新到默认元数据
|
299
|
+
self.update_default_metadata(**self._request_context.to_metadata())
|
300
|
+
|
301
|
+
def get_full_context(self) -> FullContext:
|
302
|
+
"""获取完整的上下文信息"""
|
303
|
+
return FullContext(
|
304
|
+
user_context=self._user_context,
|
305
|
+
request_context=self._request_context
|
306
|
+
)
|
307
|
+
|
308
|
+
def __enter__(self) -> "SyncGrpcClient":
|
309
|
+
self.connect()
|
310
|
+
return self
|
311
|
+
|
312
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
313
|
+
self.close()
|
@@ -0,0 +1,43 @@
|
|
1
|
+
"""
|
2
|
+
数据模型定义
|
3
|
+
"""
|
4
|
+
from .file import (
|
5
|
+
File,
|
6
|
+
UploadFile,
|
7
|
+
FileUploadResponse,
|
8
|
+
UploadUrlResponse,
|
9
|
+
ShareLinkRequest,
|
10
|
+
FileVisitRequest,
|
11
|
+
FileListRequest,
|
12
|
+
FileListResponse,
|
13
|
+
)
|
14
|
+
from .folder import (
|
15
|
+
FolderInfo,
|
16
|
+
FolderListResponse,
|
17
|
+
)
|
18
|
+
from .context import (
|
19
|
+
UserContext,
|
20
|
+
RequestContext,
|
21
|
+
FullContext,
|
22
|
+
)
|
23
|
+
|
24
|
+
__all__ = [
|
25
|
+
# 文件相关
|
26
|
+
"File",
|
27
|
+
"UploadFile",
|
28
|
+
"FileUploadResponse",
|
29
|
+
"UploadUrlResponse",
|
30
|
+
"ShareLinkRequest",
|
31
|
+
"FileVisitRequest",
|
32
|
+
"FileListRequest",
|
33
|
+
"FileListResponse",
|
34
|
+
|
35
|
+
# 文件夹相关
|
36
|
+
"FolderInfo",
|
37
|
+
"FolderListResponse",
|
38
|
+
|
39
|
+
# 上下文相关
|
40
|
+
"UserContext",
|
41
|
+
"RequestContext",
|
42
|
+
"FullContext",
|
43
|
+
]
|
@@ -0,0 +1,160 @@
|
|
1
|
+
"""
|
2
|
+
用户上下文和请求上下文相关的Schema定义
|
3
|
+
"""
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
from typing import Optional, Dict, Any
|
6
|
+
from datetime import datetime
|
7
|
+
|
8
|
+
from file_hub_client.enums import Role
|
9
|
+
|
10
|
+
|
11
|
+
@dataclass
|
12
|
+
class UserContext:
|
13
|
+
"""
|
14
|
+
用户上下文信息
|
15
|
+
|
16
|
+
包含两大类信息:
|
17
|
+
1. ownership(所有权): org_id, user_id - 表示资源归属
|
18
|
+
2. operator(操作者): actor_id, role - 表示实际操作者(可能是用户、agent或系统)
|
19
|
+
"""
|
20
|
+
# Ownership - 资源所有权信息
|
21
|
+
org_id: str # 组织ID
|
22
|
+
user_id: str # 用户ID
|
23
|
+
|
24
|
+
# Operator - 操作者信息
|
25
|
+
actor_id: Optional[str] = None # 实际操作者ID(如果为空,默认使用user_id)
|
26
|
+
role: Role = Role.ACCOUNT # 操作者角色(ACCOUNT, AGENT, SYSTEM等)
|
27
|
+
|
28
|
+
def __post_init__(self):
|
29
|
+
"""初始化后处理,如果actor_id为空,默认使用user_id"""
|
30
|
+
if self.actor_id is None:
|
31
|
+
self.actor_id = self.user_id
|
32
|
+
|
33
|
+
def to_metadata(self) -> Dict[str, str]:
|
34
|
+
"""转换为gRPC metadata格式"""
|
35
|
+
return {
|
36
|
+
'x-org-id': self.org_id,
|
37
|
+
'x-user-id': self.user_id,
|
38
|
+
'x-actor-id': self.actor_id,
|
39
|
+
'x-role': self.role,
|
40
|
+
}
|
41
|
+
|
42
|
+
@classmethod
|
43
|
+
def from_metadata(cls, metadata: Dict[str, str]) -> Optional['UserContext']:
|
44
|
+
"""从metadata中解析用户上下文"""
|
45
|
+
org_id = metadata.get('x-org-id')
|
46
|
+
user_id = metadata.get('x-user-id')
|
47
|
+
|
48
|
+
if not org_id or not user_id:
|
49
|
+
return None
|
50
|
+
|
51
|
+
return cls(
|
52
|
+
org_id=org_id,
|
53
|
+
user_id=user_id,
|
54
|
+
actor_id=metadata.get('x-actor-id'),
|
55
|
+
role=Role(metadata.get('x-role', Role.ACCOUNT))
|
56
|
+
)
|
57
|
+
|
58
|
+
|
59
|
+
@dataclass
|
60
|
+
class RequestContext:
|
61
|
+
"""
|
62
|
+
请求上下文信息
|
63
|
+
|
64
|
+
包含请求相关的元数据,如客户端信息、请求追踪等
|
65
|
+
"""
|
66
|
+
request_id: Optional[str] = None # 请求ID,用于追踪
|
67
|
+
client_ip: Optional[str] = None # 客户端IP地址
|
68
|
+
client_version: Optional[str] = None # 客户端版本
|
69
|
+
client_type: Optional[str] = None # 客户端类型(web, mobile, desktop, cli等)
|
70
|
+
user_agent: Optional[str] = None # User-Agent信息
|
71
|
+
timestamp: Optional[datetime] = field(default_factory=datetime.now) # 请求时间戳
|
72
|
+
extra: Dict[str, Any] = field(default_factory=dict) # 其他扩展信息
|
73
|
+
|
74
|
+
def to_metadata(self) -> Dict[str, str]:
|
75
|
+
"""转换为gRPC metadata格式"""
|
76
|
+
metadata = {}
|
77
|
+
|
78
|
+
if self.request_id:
|
79
|
+
metadata['x-request-id'] = self.request_id
|
80
|
+
if self.client_ip:
|
81
|
+
metadata['x-client-ip'] = self.client_ip
|
82
|
+
if self.client_version:
|
83
|
+
metadata['x-client-version'] = self.client_version
|
84
|
+
if self.client_type:
|
85
|
+
metadata['x-client-type'] = self.client_type
|
86
|
+
if self.user_agent:
|
87
|
+
metadata['x-user-agent'] = self.user_agent
|
88
|
+
if self.timestamp:
|
89
|
+
metadata['x-timestamp'] = self.timestamp.isoformat()
|
90
|
+
|
91
|
+
# 添加扩展信息
|
92
|
+
for key, value in self.extra.items():
|
93
|
+
metadata[f'x-{key}'] = str(value)
|
94
|
+
|
95
|
+
return metadata
|
96
|
+
|
97
|
+
@classmethod
|
98
|
+
def from_metadata(cls, metadata: Dict[str, str]) -> 'RequestContext':
|
99
|
+
"""从metadata中解析请求上下文"""
|
100
|
+
# 提取标准字段
|
101
|
+
request_id = metadata.get('x-request-id')
|
102
|
+
client_ip = metadata.get('x-client-ip')
|
103
|
+
client_version = metadata.get('x-client-version')
|
104
|
+
client_type = metadata.get('x-client-type')
|
105
|
+
user_agent = metadata.get('x-user-agent')
|
106
|
+
|
107
|
+
# 解析时间戳
|
108
|
+
timestamp = None
|
109
|
+
if 'x-timestamp' in metadata:
|
110
|
+
try:
|
111
|
+
timestamp = datetime.fromisoformat(metadata['x-timestamp'])
|
112
|
+
except:
|
113
|
+
pass
|
114
|
+
|
115
|
+
# 提取扩展字段
|
116
|
+
extra = {}
|
117
|
+
for key, value in metadata.items():
|
118
|
+
if key.startswith('x-') and key not in [
|
119
|
+
'x-request-id', 'x-client-ip', 'x-client-version',
|
120
|
+
'x-client-type', 'x-user-agent', 'x-timestamp',
|
121
|
+
'x-org-id', 'x-user-id', 'x-actor-id', 'x-role'
|
122
|
+
]:
|
123
|
+
extra[key[2:]] = value # 去掉 'x-' 前缀
|
124
|
+
|
125
|
+
return cls(
|
126
|
+
request_id=request_id,
|
127
|
+
client_ip=client_ip,
|
128
|
+
client_version=client_version,
|
129
|
+
client_type=client_type,
|
130
|
+
user_agent=user_agent,
|
131
|
+
timestamp=timestamp,
|
132
|
+
extra=extra
|
133
|
+
)
|
134
|
+
|
135
|
+
|
136
|
+
@dataclass
|
137
|
+
class FullContext:
|
138
|
+
"""完整的上下文信息,包含用户上下文和请求上下文"""
|
139
|
+
user_context: Optional[UserContext] = None
|
140
|
+
request_context: Optional[RequestContext] = None
|
141
|
+
|
142
|
+
def to_metadata(self) -> Dict[str, str]:
|
143
|
+
"""转换为gRPC metadata格式"""
|
144
|
+
metadata = {}
|
145
|
+
|
146
|
+
if self.user_context:
|
147
|
+
metadata.update(self.user_context.to_metadata())
|
148
|
+
|
149
|
+
if self.request_context:
|
150
|
+
metadata.update(self.request_context.to_metadata())
|
151
|
+
|
152
|
+
return metadata
|
153
|
+
|
154
|
+
@classmethod
|
155
|
+
def from_metadata(cls, metadata: Dict[str, str]) -> 'FullContext':
|
156
|
+
"""从metadata中解析完整上下文"""
|
157
|
+
return cls(
|
158
|
+
user_context=UserContext.from_metadata(metadata),
|
159
|
+
request_context=RequestContext.from_metadata(metadata)
|
160
|
+
)
|
@@ -0,0 +1,89 @@
|
|
1
|
+
"""
|
2
|
+
文件相关数据模型
|
3
|
+
"""
|
4
|
+
from datetime import datetime
|
5
|
+
from typing import Optional, Dict, List, Any
|
6
|
+
from pydantic import BaseModel, Field
|
7
|
+
|
8
|
+
|
9
|
+
class File(BaseModel):
|
10
|
+
"""文件信息模型"""
|
11
|
+
id: str = Field(..., description="文件ID")
|
12
|
+
folder_id: str = Field(..., description="所属文件夹ID")
|
13
|
+
file_name: str = Field(..., description="原始文件名")
|
14
|
+
file_type: str = Field(..., description="文件类型")
|
15
|
+
created_at: datetime = Field(..., description="创建时间")
|
16
|
+
updated_at: datetime = Field(..., description="更新时间")
|
17
|
+
|
18
|
+
class Config:
|
19
|
+
json_encoders = {
|
20
|
+
datetime: lambda v: v.isoformat()
|
21
|
+
}
|
22
|
+
|
23
|
+
|
24
|
+
class UploadFile(BaseModel):
|
25
|
+
"""上传文件信息模型"""
|
26
|
+
id: str = Field(..., description="上传文件ID")
|
27
|
+
folder_id: str = Field(..., description="所属文件夹ID")
|
28
|
+
storage_type: str = Field(..., description="存储类型")
|
29
|
+
stored_name: str = Field(..., description="存储文件名")
|
30
|
+
stored_path: str = Field(..., description="存储路径")
|
31
|
+
file_id: str = Field(..., description="所属文件ID")
|
32
|
+
file_name: str = Field(..., description="原始文件名")
|
33
|
+
file_size: int = Field(0, description="文件大小(字节)")
|
34
|
+
file_ext: str = Field(..., description="文件后缀")
|
35
|
+
mime_type: str = Field(..., description="MIME类型")
|
36
|
+
created_at: datetime = Field(..., description="创建时间")
|
37
|
+
updated_at: datetime = Field(..., description="更新时间")
|
38
|
+
|
39
|
+
class Config:
|
40
|
+
json_encoders = {
|
41
|
+
datetime: lambda v: v.isoformat()
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
class FileUploadResponse(BaseModel):
|
46
|
+
"""文件上传返回"""
|
47
|
+
file: File = Field(..., description="文件信息")
|
48
|
+
upload_file: UploadFile = Field(..., description="上传文件信息")
|
49
|
+
|
50
|
+
|
51
|
+
class UploadUrlResponse(BaseModel):
|
52
|
+
"""上传URL响应"""
|
53
|
+
file: File = Field(..., description="文件信息")
|
54
|
+
upload_file: UploadFile = Field(..., description="上传文件信息")
|
55
|
+
upload_url: str = Field(..., description="上传URL")
|
56
|
+
|
57
|
+
|
58
|
+
class ShareLinkRequest(BaseModel):
|
59
|
+
"""生成分享链接请求"""
|
60
|
+
file_id: str = Field(..., description="文件ID")
|
61
|
+
is_public: bool = Field(True, description="是否公开")
|
62
|
+
access_scope: str = Field("view", description="访问范围")
|
63
|
+
expire_seconds: int = Field(86400, description="过期时间(秒)")
|
64
|
+
max_access: Optional[int] = Field(None, description="最大访问次数")
|
65
|
+
password: Optional[str] = Field(None, description="访问密码")
|
66
|
+
|
67
|
+
|
68
|
+
class FileVisitRequest(BaseModel):
|
69
|
+
"""文件访问请求"""
|
70
|
+
file_share_id: str = Field(..., description="分享ID")
|
71
|
+
access_type: str = Field(..., description="访问类型")
|
72
|
+
access_duration: int = Field(..., description="访问时长")
|
73
|
+
metadata: Dict[str, Any] = Field(default_factory=dict, description="元数据")
|
74
|
+
|
75
|
+
|
76
|
+
class FileListRequest(BaseModel):
|
77
|
+
"""文件列表请求"""
|
78
|
+
folder_id: str = Field(..., description="文件夹ID")
|
79
|
+
file_name: Optional[str] = Field(None, description="文件名")
|
80
|
+
file_type: Optional[List[str]] = Field(None, description="文件类型")
|
81
|
+
created_by_role: Optional[str] = Field(None, description="创建者角色")
|
82
|
+
created_by: Optional[str] = Field(None, description="创建者")
|
83
|
+
page_size: int = Field(20, description="每页大小")
|
84
|
+
page: int = Field(1, description="页码")
|
85
|
+
|
86
|
+
|
87
|
+
class FileListResponse(BaseModel):
|
88
|
+
"""文件列表响应"""
|
89
|
+
files: List[File] = Field(default_factory=list, description="文件列表")
|
@@ -0,0 +1,29 @@
|
|
1
|
+
"""
|
2
|
+
文件夹相关数据模型
|
3
|
+
"""
|
4
|
+
from datetime import datetime
|
5
|
+
from typing import List
|
6
|
+
from pydantic import BaseModel, Field
|
7
|
+
|
8
|
+
|
9
|
+
class FolderInfo(BaseModel):
|
10
|
+
"""文件夹信息模型"""
|
11
|
+
id: str = Field(..., description="文件夹ID")
|
12
|
+
org_id: str = Field(..., description="组织ID")
|
13
|
+
user_id: str = Field(..., description="用户ID")
|
14
|
+
folder_name: str = Field(..., description="文件夹名称")
|
15
|
+
parent_id: str = Field(..., description="父文件夹ID")
|
16
|
+
created_by: str = Field(..., description="创建者")
|
17
|
+
created_by_role: str = Field(..., description="创建者角色")
|
18
|
+
created_at: datetime = Field(..., description="创建时间")
|
19
|
+
updated_at: datetime = Field(..., description="更新时间")
|
20
|
+
|
21
|
+
class Config:
|
22
|
+
json_encoders = {
|
23
|
+
datetime: lambda v: v.isoformat()
|
24
|
+
}
|
25
|
+
|
26
|
+
|
27
|
+
class FolderListResponse(BaseModel):
|
28
|
+
"""文件夹列表响应"""
|
29
|
+
items: List[FolderInfo] = Field(default_factory=list, description="文件夹列表")
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
服务模块
|
3
|
+
"""
|
4
|
+
from .file import AsyncBlobService, AsyncFileService, SyncBlobService, SyncFileService
|
5
|
+
from .folder import AsyncFolderService, SyncFolderService
|
6
|
+
|
7
|
+
__all__ = [
|
8
|
+
# 文件服务
|
9
|
+
"AsyncBlobService",
|
10
|
+
"AsyncFileService",
|
11
|
+
"SyncBlobService",
|
12
|
+
"SyncFileService",
|
13
|
+
|
14
|
+
# 文件夹服务
|
15
|
+
"AsyncFolderService",
|
16
|
+
"SyncFolderService",
|
17
|
+
]
|
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
文件服务模块
|
3
|
+
"""
|
4
|
+
from .async_blob_service import AsyncBlobService
|
5
|
+
from .async_file_service import AsyncFileService
|
6
|
+
from .sync_blob_service import SyncBlobService
|
7
|
+
from .sync_file_service import SyncFileService
|
8
|
+
|
9
|
+
__all__ = [
|
10
|
+
"AsyncBlobService",
|
11
|
+
"AsyncFileService",
|
12
|
+
"SyncBlobService",
|
13
|
+
"SyncFileService",
|
14
|
+
]
|