tamar-file-hub-client 0.1.3__py3-none-any.whl → 0.1.5__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/client.py +24 -4
- file_hub_client/rpc/async_client.py +31 -4
- file_hub_client/rpc/gen/file_service_pb2.py +30 -12
- file_hub_client/rpc/gen/file_service_pb2_grpc.py +173 -0
- file_hub_client/rpc/interceptors.py +578 -580
- file_hub_client/rpc/protos/file_service.proto +68 -1
- file_hub_client/rpc/sync_client.py +31 -4
- file_hub_client/schemas/__init__.py +10 -0
- file_hub_client/schemas/context.py +171 -160
- file_hub_client/schemas/file.py +171 -126
- file_hub_client/services/file/async_blob_service.py +260 -8
- file_hub_client/services/file/async_file_service.py +217 -0
- file_hub_client/services/file/sync_blob_service.py +261 -8
- file_hub_client/services/file/sync_file_service.py +217 -0
- file_hub_client/utils/__init__.py +14 -0
- file_hub_client/utils/file_utils.py +186 -153
- file_hub_client/utils/ip_detector.py +226 -0
- file_hub_client/utils/logging.py +335 -318
- {tamar_file_hub_client-0.1.3.dist-info → tamar_file_hub_client-0.1.5.dist-info}/METADATA +178 -2
- {tamar_file_hub_client-0.1.3.dist-info → tamar_file_hub_client-0.1.5.dist-info}/RECORD +22 -21
- {tamar_file_hub_client-0.1.3.dist-info → tamar_file_hub_client-0.1.5.dist-info}/WHEEL +0 -0
- {tamar_file_hub_client-0.1.3.dist-info → tamar_file_hub_client-0.1.5.dist-info}/top_level.txt +0 -0
@@ -22,6 +22,12 @@ service FileService {
|
|
22
22
|
rpc RenameFile (RenameFileRequest) returns (File);
|
23
23
|
rpc DeleteFile (DeleteFileRequest) returns (Empty);
|
24
24
|
rpc ListFiles (ListFilesRequest) returns (FileListResponse);
|
25
|
+
|
26
|
+
// 压缩服务相关API
|
27
|
+
rpc GetCompressionStatus (CompressionStatusRequest) returns (CompressionStatusResponse);
|
28
|
+
rpc GetCompressedVariants (GetVariantsRequest) returns (GetVariantsResponse);
|
29
|
+
rpc TriggerRecompression (RecompressionRequest) returns (RecompressionResponse);
|
30
|
+
rpc GenerateVariantDownloadUrl (VariantDownloadUrlRequest) returns (VariantDownloadUrlResponse);
|
25
31
|
}
|
26
32
|
|
27
33
|
// ========= 数据结构定义 =========
|
@@ -176,7 +182,8 @@ message BatchDownloadUrlResponse {
|
|
176
182
|
message DownloadUrlInfo {
|
177
183
|
string file_id = 1;
|
178
184
|
string url = 2;
|
179
|
-
|
185
|
+
string mime_type = 3;
|
186
|
+
optional string error = 4;
|
180
187
|
}
|
181
188
|
|
182
189
|
message GetGcsUrlResponse {
|
@@ -195,4 +202,64 @@ message GcsUrlInfo {
|
|
195
202
|
optional string error = 4;
|
196
203
|
}
|
197
204
|
|
205
|
+
// ========= 压缩服务相关结构 =========
|
206
|
+
|
207
|
+
message CompressionStatusRequest {
|
208
|
+
string file_id = 1;
|
209
|
+
}
|
210
|
+
|
211
|
+
message CompressionStatusResponse {
|
212
|
+
string status = 1; // pending, processing, completed, failed
|
213
|
+
optional string error_message = 2;
|
214
|
+
repeated CompressedVariant variants = 3;
|
215
|
+
}
|
216
|
+
|
217
|
+
message GetVariantsRequest {
|
218
|
+
string file_id = 1;
|
219
|
+
optional string variant_type = 2; // image, video, thumbnail
|
220
|
+
}
|
221
|
+
|
222
|
+
message GetVariantsResponse {
|
223
|
+
repeated CompressedVariant variants = 1;
|
224
|
+
}
|
225
|
+
|
226
|
+
message CompressedVariant {
|
227
|
+
string variant_name = 1;
|
228
|
+
string variant_type = 2;
|
229
|
+
string media_type = 3;
|
230
|
+
int32 width = 4;
|
231
|
+
int32 height = 5;
|
232
|
+
int64 file_size = 6;
|
233
|
+
string format = 7;
|
234
|
+
optional int32 quality = 8;
|
235
|
+
optional double duration = 9;
|
236
|
+
optional int64 bitrate = 10;
|
237
|
+
optional int32 fps = 11;
|
238
|
+
double compression_ratio = 12;
|
239
|
+
string stored_path = 13;
|
240
|
+
}
|
241
|
+
|
242
|
+
message RecompressionRequest {
|
243
|
+
string file_id = 1;
|
244
|
+
optional bool force_reprocess = 2;
|
245
|
+
}
|
246
|
+
|
247
|
+
message RecompressionResponse {
|
248
|
+
string task_id = 1;
|
249
|
+
string status = 2;
|
250
|
+
}
|
251
|
+
|
252
|
+
message VariantDownloadUrlRequest {
|
253
|
+
string file_id = 1;
|
254
|
+
string variant_name = 2; // large/medium/small/thumbnail
|
255
|
+
optional int32 expire_seconds = 3;
|
256
|
+
optional bool is_cdn = 4;
|
257
|
+
}
|
258
|
+
|
259
|
+
message VariantDownloadUrlResponse {
|
260
|
+
string url = 1;
|
261
|
+
optional string error = 2;
|
262
|
+
optional CompressedVariant variant_info = 3; // 返回变体详细信息
|
263
|
+
}
|
264
|
+
|
198
265
|
message Empty {}
|
@@ -284,8 +284,17 @@ class SyncGrpcClient:
|
|
284
284
|
# 添加默认元数据
|
285
285
|
metadata.update(self.default_metadata)
|
286
286
|
|
287
|
-
#
|
288
|
-
|
287
|
+
# 自动检测用户真实IP
|
288
|
+
from ..utils.ip_detector import get_current_user_ip
|
289
|
+
auto_detected_ip = get_current_user_ip()
|
290
|
+
if auto_detected_ip and 'x-user-ip' not in metadata:
|
291
|
+
# 只有在没有设置过user_ip的情况下才使用自动检测的IP
|
292
|
+
metadata['x-user-ip'] = auto_detected_ip
|
293
|
+
|
294
|
+
# 添加/覆盖传入的元数据,但跳过None值以避免覆盖有效的默认值
|
295
|
+
for k, v in kwargs.items():
|
296
|
+
if v is not None:
|
297
|
+
metadata[k] = v
|
289
298
|
|
290
299
|
# 处理 request_id(优先级:显式传入 > metadata中的x-request-id > RequestContext > 自动生成)
|
291
300
|
if request_id is not None:
|
@@ -317,7 +326,7 @@ class SyncGrpcClient:
|
|
317
326
|
"""
|
318
327
|
self.default_metadata.update(kwargs)
|
319
328
|
|
320
|
-
def set_user_context(self, org_id: str, user_id: str, role: Role = Role.ACCOUNT, actor_id: Optional[str] = None):
|
329
|
+
def set_user_context(self, org_id: str, user_id: str, role: Role = Role.ACCOUNT, actor_id: Optional[str] = None, user_ip: Optional[str] = None):
|
321
330
|
"""
|
322
331
|
设置用户上下文信息
|
323
332
|
|
@@ -326,16 +335,34 @@ class SyncGrpcClient:
|
|
326
335
|
user_id: 用户ID
|
327
336
|
role: 用户角色(默认为 ACCOUNT)
|
328
337
|
actor_id: 操作者ID(如果不同于 user_id)
|
338
|
+
user_ip: 用户IP地址(实际请求用户的IP,如前端用户的IP)
|
329
339
|
"""
|
330
340
|
self._user_context = UserContext(
|
331
341
|
org_id=org_id,
|
332
342
|
user_id=user_id,
|
333
343
|
role=role,
|
334
|
-
actor_id=actor_id
|
344
|
+
actor_id=actor_id,
|
345
|
+
user_ip=user_ip
|
335
346
|
)
|
336
347
|
# 更新到默认元数据
|
337
348
|
self.update_default_metadata(**self._user_context.to_metadata())
|
338
349
|
|
350
|
+
def set_user_ip(self, user_ip: Optional[str]):
|
351
|
+
"""
|
352
|
+
设置或更新用户IP地址
|
353
|
+
|
354
|
+
Args:
|
355
|
+
user_ip: 用户IP地址(实际请求用户的IP,如前端用户的IP)
|
356
|
+
"""
|
357
|
+
if self._user_context:
|
358
|
+
self._user_context.user_ip = user_ip
|
359
|
+
# 先移除旧的x-user-ip(如果存在)
|
360
|
+
self.default_metadata.pop('x-user-ip', None)
|
361
|
+
# 更新到默认元数据(只有非None值会被添加)
|
362
|
+
self.update_default_metadata(**self._user_context.to_metadata())
|
363
|
+
else:
|
364
|
+
raise ValueError("必须先调用 set_user_context 设置用户上下文,然后才能设置用户IP")
|
365
|
+
|
339
366
|
def get_user_context(self) -> Optional[UserContext]:
|
340
367
|
"""获取当前用户上下文"""
|
341
368
|
return self._user_context
|
@@ -16,6 +16,11 @@ from .file import (
|
|
16
16
|
GcsUrlInfo,
|
17
17
|
GetGcsUrlResponse,
|
18
18
|
BatchGcsUrlResponse,
|
19
|
+
CompressedVariant,
|
20
|
+
CompressionStatusResponse,
|
21
|
+
GetVariantsResponse,
|
22
|
+
RecompressionResponse,
|
23
|
+
VariantDownloadUrlResponse,
|
19
24
|
)
|
20
25
|
from .folder import (
|
21
26
|
FolderInfo,
|
@@ -73,6 +78,11 @@ __all__ = [
|
|
73
78
|
"GcsUrlInfo",
|
74
79
|
"GetGcsUrlResponse",
|
75
80
|
"BatchGcsUrlResponse",
|
81
|
+
"CompressedVariant",
|
82
|
+
"CompressionStatusResponse",
|
83
|
+
"GetVariantsResponse",
|
84
|
+
"RecompressionResponse",
|
85
|
+
"VariantDownloadUrlResponse",
|
76
86
|
|
77
87
|
# 文件夹相关
|
78
88
|
"FolderInfo",
|
@@ -1,160 +1,171 @@
|
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
)
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
user_context
|
159
|
-
|
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
|
+
3. request info(请求信息): user_ip - 表示请求来源IP(用于审计和安全)
|
20
|
+
"""
|
21
|
+
# Ownership - 资源所有权信息
|
22
|
+
org_id: str # 组织ID
|
23
|
+
user_id: str # 用户ID
|
24
|
+
|
25
|
+
# Operator - 操作者信息
|
26
|
+
actor_id: Optional[str] = None # 实际操作者ID(如果为空,默认使用user_id)
|
27
|
+
role: Role = Role.ACCOUNT # 操作者角色(ACCOUNT, AGENT, SYSTEM等)
|
28
|
+
|
29
|
+
# Request info - 请求信息
|
30
|
+
user_ip: Optional[str] = None # 用户IP地址(请求来源IP)
|
31
|
+
|
32
|
+
def __post_init__(self):
|
33
|
+
"""初始化后处理,如果actor_id为空,默认使用user_id"""
|
34
|
+
if self.actor_id is None:
|
35
|
+
self.actor_id = self.user_id
|
36
|
+
|
37
|
+
def to_metadata(self) -> Dict[str, str]:
|
38
|
+
"""转换为gRPC metadata格式"""
|
39
|
+
metadata = {
|
40
|
+
'x-org-id': self.org_id,
|
41
|
+
'x-user-id': self.user_id,
|
42
|
+
'x-actor-id': self.actor_id,
|
43
|
+
'x-role': self.role,
|
44
|
+
}
|
45
|
+
|
46
|
+
# 只有当user_ip不为None时才添加x-user-ip
|
47
|
+
if self.user_ip is not None:
|
48
|
+
metadata['x-user-ip'] = self.user_ip
|
49
|
+
|
50
|
+
return metadata
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def from_metadata(cls, metadata: Dict[str, str]) -> Optional['UserContext']:
|
54
|
+
"""从metadata中解析用户上下文"""
|
55
|
+
org_id = metadata.get('x-org-id')
|
56
|
+
user_id = metadata.get('x-user-id')
|
57
|
+
|
58
|
+
if not org_id or not user_id:
|
59
|
+
return None
|
60
|
+
|
61
|
+
return cls(
|
62
|
+
org_id=org_id,
|
63
|
+
user_id=user_id,
|
64
|
+
actor_id=metadata.get('x-actor-id'),
|
65
|
+
role=Role(metadata.get('x-role', Role.ACCOUNT)),
|
66
|
+
user_ip=metadata.get('x-user-ip')
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
@dataclass
|
71
|
+
class RequestContext:
|
72
|
+
"""
|
73
|
+
请求上下文信息
|
74
|
+
|
75
|
+
包含请求相关的元数据,如客户端信息、请求追踪等
|
76
|
+
"""
|
77
|
+
request_id: Optional[str] = None # 请求ID,用于追踪
|
78
|
+
client_ip: Optional[str] = None # 客户端IP地址
|
79
|
+
client_version: Optional[str] = None # 客户端版本
|
80
|
+
client_type: Optional[str] = None # 客户端类型(web, mobile, desktop, cli等)
|
81
|
+
user_agent: Optional[str] = None # User-Agent信息
|
82
|
+
timestamp: Optional[datetime] = field(default_factory=datetime.now) # 请求时间戳
|
83
|
+
extra: Dict[str, Any] = field(default_factory=dict) # 其他扩展信息
|
84
|
+
|
85
|
+
def to_metadata(self) -> Dict[str, str]:
|
86
|
+
"""转换为gRPC metadata格式"""
|
87
|
+
metadata = {}
|
88
|
+
|
89
|
+
if self.request_id:
|
90
|
+
metadata['x-request-id'] = self.request_id
|
91
|
+
if self.client_ip:
|
92
|
+
metadata['x-client-ip'] = self.client_ip
|
93
|
+
if self.client_version:
|
94
|
+
metadata['x-client-version'] = self.client_version
|
95
|
+
if self.client_type:
|
96
|
+
metadata['x-client-type'] = self.client_type
|
97
|
+
if self.user_agent:
|
98
|
+
metadata['x-user-agent'] = self.user_agent
|
99
|
+
if self.timestamp:
|
100
|
+
metadata['x-timestamp'] = self.timestamp.isoformat()
|
101
|
+
|
102
|
+
# 添加扩展信息
|
103
|
+
for key, value in self.extra.items():
|
104
|
+
metadata[f'x-{key}'] = str(value)
|
105
|
+
|
106
|
+
return metadata
|
107
|
+
|
108
|
+
@classmethod
|
109
|
+
def from_metadata(cls, metadata: Dict[str, str]) -> 'RequestContext':
|
110
|
+
"""从metadata中解析请求上下文"""
|
111
|
+
# 提取标准字段
|
112
|
+
request_id = metadata.get('x-request-id')
|
113
|
+
client_ip = metadata.get('x-client-ip')
|
114
|
+
client_version = metadata.get('x-client-version')
|
115
|
+
client_type = metadata.get('x-client-type')
|
116
|
+
user_agent = metadata.get('x-user-agent')
|
117
|
+
|
118
|
+
# 解析时间戳
|
119
|
+
timestamp = None
|
120
|
+
if 'x-timestamp' in metadata:
|
121
|
+
try:
|
122
|
+
timestamp = datetime.fromisoformat(metadata['x-timestamp'])
|
123
|
+
except:
|
124
|
+
pass
|
125
|
+
|
126
|
+
# 提取扩展字段
|
127
|
+
extra = {}
|
128
|
+
for key, value in metadata.items():
|
129
|
+
if key.startswith('x-') and key not in [
|
130
|
+
'x-request-id', 'x-client-ip', 'x-client-version',
|
131
|
+
'x-client-type', 'x-user-agent', 'x-timestamp',
|
132
|
+
'x-org-id', 'x-user-id', 'x-actor-id', 'x-role'
|
133
|
+
]:
|
134
|
+
extra[key[2:]] = value # 去掉 'x-' 前缀
|
135
|
+
|
136
|
+
return cls(
|
137
|
+
request_id=request_id,
|
138
|
+
client_ip=client_ip,
|
139
|
+
client_version=client_version,
|
140
|
+
client_type=client_type,
|
141
|
+
user_agent=user_agent,
|
142
|
+
timestamp=timestamp,
|
143
|
+
extra=extra
|
144
|
+
)
|
145
|
+
|
146
|
+
|
147
|
+
@dataclass
|
148
|
+
class FullContext:
|
149
|
+
"""完整的上下文信息,包含用户上下文和请求上下文"""
|
150
|
+
user_context: Optional[UserContext] = None
|
151
|
+
request_context: Optional[RequestContext] = None
|
152
|
+
|
153
|
+
def to_metadata(self) -> Dict[str, str]:
|
154
|
+
"""转换为gRPC metadata格式"""
|
155
|
+
metadata = {}
|
156
|
+
|
157
|
+
if self.user_context:
|
158
|
+
metadata.update(self.user_context.to_metadata())
|
159
|
+
|
160
|
+
if self.request_context:
|
161
|
+
metadata.update(self.request_context.to_metadata())
|
162
|
+
|
163
|
+
return metadata
|
164
|
+
|
165
|
+
@classmethod
|
166
|
+
def from_metadata(cls, metadata: Dict[str, str]) -> 'FullContext':
|
167
|
+
"""从metadata中解析完整上下文"""
|
168
|
+
return cls(
|
169
|
+
user_context=UserContext.from_metadata(metadata),
|
170
|
+
request_context=RequestContext.from_metadata(metadata)
|
171
|
+
)
|