adp-operator 1.0.7__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 (83) hide show
  1. adp_operator-1.0.7/PKG-INFO +81 -0
  2. adp_operator-1.0.7/README.md +57 -0
  3. adp_operator-1.0.7/adp_operator/__init__.py +0 -0
  4. adp_operator-1.0.7/adp_operator/c2c/__init__.py +6 -0
  5. adp_operator-1.0.7/adp_operator/c2c/__main__.py +5 -0
  6. adp_operator-1.0.7/adp_operator/c2c/client.py +128 -0
  7. adp_operator-1.0.7/adp_operator/c2c/commands.py +181 -0
  8. adp_operator-1.0.7/adp_operator/c2c/config.py +11 -0
  9. adp_operator-1.0.7/adp_operator/c2c/discovery.py +314 -0
  10. adp_operator-1.0.7/adp_operator/c2c/event_bus.py +129 -0
  11. adp_operator-1.0.7/adp_operator/c2c/handlers/__init__.py +2 -0
  12. adp_operator-1.0.7/adp_operator/c2c/handlers/data.py +11 -0
  13. adp_operator-1.0.7/adp_operator/c2c/handlers/discovery.py +126 -0
  14. adp_operator-1.0.7/adp_operator/c2c/handlers/file.py +179 -0
  15. adp_operator-1.0.7/adp_operator/c2c/handlers/identity.py +25 -0
  16. adp_operator-1.0.7/adp_operator/c2c/handlers/operator.py +90 -0
  17. adp_operator-1.0.7/adp_operator/c2c/handlers/scan.py +58 -0
  18. adp_operator-1.0.7/adp_operator/c2c/log_stream.py +68 -0
  19. adp_operator-1.0.7/adp_operator/c2c/main.py +46 -0
  20. adp_operator-1.0.7/adp_operator/c2c/node_context.py +116 -0
  21. adp_operator-1.0.7/adp_operator/c2c/operator.py +70 -0
  22. adp_operator-1.0.7/adp_operator/c2c/operator_executor.py +468 -0
  23. adp_operator-1.0.7/adp_operator/c2c/operator_loader.py +25 -0
  24. adp_operator-1.0.7/adp_operator/c2c/operator_metadata.py +34 -0
  25. adp_operator-1.0.7/adp_operator/c2c/operator_service.py +52 -0
  26. adp_operator-1.0.7/adp_operator/c2c/operator_source.py +57 -0
  27. adp_operator-1.0.7/adp_operator/c2c/peer_registry.py +388 -0
  28. adp_operator-1.0.7/adp_operator/c2c/protocol.py +210 -0
  29. adp_operator-1.0.7/adp_operator/c2c/reverse_connection.py +538 -0
  30. adp_operator-1.0.7/adp_operator/c2c/routing.py +180 -0
  31. adp_operator-1.0.7/adp_operator/c2c/run_queue.py +40 -0
  32. adp_operator-1.0.7/adp_operator/c2c/runtime.py +296 -0
  33. adp_operator-1.0.7/adp_operator/c2c/service_discovery.py +58 -0
  34. adp_operator-1.0.7/adp_operator/c2c/session.py +100 -0
  35. adp_operator-1.0.7/adp_operator/c2c/socket_channel.py +322 -0
  36. adp_operator-1.0.7/adp_operator/c2c/state.py +58 -0
  37. adp_operator-1.0.7/adp_operator/c2c/tcp_server.py +411 -0
  38. adp_operator-1.0.7/adp_operator/c2c/websocket_server.py +240 -0
  39. adp_operator-1.0.7/adp_operator/cache_utils.py +35 -0
  40. adp_operator-1.0.7/adp_operator/connect.py +91 -0
  41. adp_operator-1.0.7/adp_operator/core/__init__.py +12 -0
  42. adp_operator-1.0.7/adp_operator/core/constants.py +15 -0
  43. adp_operator-1.0.7/adp_operator/core/json2pdf.py +271 -0
  44. adp_operator-1.0.7/adp_operator/core/llm_config.py +39 -0
  45. adp_operator-1.0.7/adp_operator/core/models.py +150 -0
  46. adp_operator-1.0.7/adp_operator/core/my_scale_db_storeage.py +35 -0
  47. adp_operator-1.0.7/adp_operator/core/object_storage/__init__.py +0 -0
  48. adp_operator-1.0.7/adp_operator/core/object_storage/base.py +45 -0
  49. adp_operator-1.0.7/adp_operator/core/object_storage/base_stream_object.py +3 -0
  50. adp_operator-1.0.7/adp_operator/core/object_storage/minio_object_storage.py +213 -0
  51. adp_operator-1.0.7/adp_operator/core/object_storage/tos_object_storage.py +225 -0
  52. adp_operator-1.0.7/adp_operator/core/operator_init_config.py +121 -0
  53. adp_operator-1.0.7/adp_operator/core/operator_utils.py +80 -0
  54. adp_operator-1.0.7/adp_operator/core/run_and_load_node_info.py +115 -0
  55. adp_operator-1.0.7/adp_operator/dataset/__init__.py +5 -0
  56. adp_operator-1.0.7/adp_operator/dataset/cache.py +189 -0
  57. adp_operator-1.0.7/adp_operator/dataset/storage.py +25 -0
  58. adp_operator-1.0.7/adp_operator/logger.py +113 -0
  59. adp_operator-1.0.7/adp_operator/management/__init__.py +1 -0
  60. adp_operator-1.0.7/adp_operator/management/commands/__init__.py +1 -0
  61. adp_operator-1.0.7/adp_operator/management/commands/common.py +43 -0
  62. adp_operator-1.0.7/adp_operator/management/commands/login.py +54 -0
  63. adp_operator-1.0.7/adp_operator/management/commands/logs.py +19 -0
  64. adp_operator-1.0.7/adp_operator/management/commands/run.py +56 -0
  65. adp_operator-1.0.7/adp_operator/management/commands/upload.py +109 -0
  66. adp_operator-1.0.7/adp_operator/management/main.py +78 -0
  67. adp_operator-1.0.7/adp_operator/myscale_pool.py +163 -0
  68. adp_operator-1.0.7/adp_operator/myscale_utils.py +90 -0
  69. adp_operator-1.0.7/adp_operator/storage.py +1184 -0
  70. adp_operator-1.0.7/adp_operator/utils/__init__.py +0 -0
  71. adp_operator-1.0.7/adp_operator/utils/dataframe_transport.py +42 -0
  72. adp_operator-1.0.7/adp_operator/utils/docker_api.py +46 -0
  73. adp_operator-1.0.7/adp_operator/utils/file_utils.py +57 -0
  74. adp_operator-1.0.7/adp_operator/utils/myscale_pool.py +187 -0
  75. adp_operator-1.0.7/adp_operator/utils/myscale_utils.py +389 -0
  76. adp_operator-1.0.7/adp_operator.egg-info/PKG-INFO +81 -0
  77. adp_operator-1.0.7/adp_operator.egg-info/SOURCES.txt +81 -0
  78. adp_operator-1.0.7/adp_operator.egg-info/dependency_links.txt +1 -0
  79. adp_operator-1.0.7/adp_operator.egg-info/entry_points.txt +2 -0
  80. adp_operator-1.0.7/adp_operator.egg-info/requires.txt +12 -0
  81. adp_operator-1.0.7/adp_operator.egg-info/top_level.txt +1 -0
  82. adp_operator-1.0.7/setup.cfg +4 -0
  83. adp_operator-1.0.7/setup.py +41 -0
@@ -0,0 +1,81 @@
1
+ Metadata-Version: 2.4
2
+ Name: adp-operator
3
+ Version: 1.0.7
4
+ Summary: ADP operator management and C2C runtime tools
5
+ Requires-Python: >=3.9
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: docker
8
+ Requires-Dist: fastapi
9
+ Requires-Dist: pandas
10
+ Requires-Dist: pyarrow
11
+ Requires-Dist: pyarmor
12
+ Requires-Dist: psutil
13
+ Requires-Dist: python-dotenv
14
+ Requires-Dist: python-multipart
15
+ Requires-Dist: requests
16
+ Requires-Dist: uvicorn
17
+ Requires-Dist: colorlog
18
+ Requires-Dist: websockets
19
+ Dynamic: description
20
+ Dynamic: description-content-type
21
+ Dynamic: requires-dist
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+ # ADP Operator
26
+
27
+ ## C2C 连接首包
28
+
29
+ C2C TCP 连接建立后,双方必须先交换固定长度的二进制首包。首包校验通过后,连接才允许继续处理 JSON 业务指令。
30
+
31
+ 首包固定为 512 bit,结构如下:
32
+
33
+ | bit 偏移 | bit 长度 | 字段 | 规则 |
34
+ | ---: | ---: | --- | --- |
35
+ | 0 | 24 | 固定标识 | 三个 ASCII 字符 `ADP` |
36
+ | 24 | 16 | 协议版本 | 16-bit 无符号整数,使用大端序,当前版本为 `7` |
37
+ | 40 | 256 | 实例编号 | 当前节点的 `instance_id`,固定32字节 |
38
+ | 296 | 8 | 功能控制区 | bit 0–2为功能开关,bit 3–7为保留位 |
39
+ | 304 | 208 | 全零保留区 | 必须全部为 `0` |
40
+
41
+ 字符串字段统一使用 UTF-8 编码。字段长度不足时在尾部使用 `0x00` 填充,超过字段长度时按字段长度截断。
42
+
43
+ ### 功能控制区
44
+
45
+ 功能控制区固定为8 bit,每个功能占用一个 bit:
46
+
47
+ | bit | 功能 | `0` | `1` | 当前默认值 |
48
+ | ---: | --- | --- | --- | --- |
49
+ | 0 | UDP 主动发现 | 关闭 | 开启 | `1` |
50
+ | 1 | 额外 channel 通道 | 不支持 | 支持 | `1` |
51
+ | 2 | WebSocket 业务端口 | 关闭 | 开启 | `1` |
52
+ | 3–7 | 保留位 | 必须为零 | 不允许 | `0` |
53
+
54
+ 当前默认控制区的二进制内容为:
55
+
56
+ ```text
57
+ 00000111
58
+ ```
59
+
60
+ ### 握手流程
61
+
62
+ 1. TCP 客户端连接 C2C 服务端。
63
+ 2. 客户端发送自己的 512 bit 二进制首包。
64
+ 3. 服务端读取并校验完整首包。
65
+ 4. 服务端校验通过后,向客户端返回自己的 512 bit 二进制首包。
66
+ 5. 客户端校验服务端首包。
67
+ 6. 双方校验均通过后,才开始发送 JSON 业务指令。
68
+
69
+ 服务端会拒绝以下连接:
70
+
71
+ - 首包长度不足 512 bit;
72
+ - 固定标识不是 `ADP`;
73
+ - 目标节点协议版本低于发起连接节点的协议版本;
74
+ - `instance_id` 为空;
75
+ - 字符串字段的补零格式错误;
76
+ - 功能控制区保留位非零;
77
+ - 208 bit 全零保留区存在非零数据。
78
+
79
+ 该首包适用于 C2C 直连、反向连接、多跳转发、额外 channel 通道、外部 `C2CSession` 和本地服务探测。未实现该二进制首包的旧节点无法与新协议节点建立连接,因此升级时需要同时更新同一网络中的全部 C2C 节点。
80
+
81
+ 协议版本字段固定为16 bit,使用大端序表示无符号整数,取值范围为 `0`–`65535`。客户端收到服务端首包后比较版本:目标节点版本大于或等于本机版本时允许连接,目标节点版本低于本机版本时拒绝连接。例如本机版本为 `4`、目标版本为 `5` 时允许连接;本机版本为 `5`、目标版本为 `4` 时拒绝连接。
@@ -0,0 +1,57 @@
1
+ # ADP Operator
2
+
3
+ ## C2C 连接首包
4
+
5
+ C2C TCP 连接建立后,双方必须先交换固定长度的二进制首包。首包校验通过后,连接才允许继续处理 JSON 业务指令。
6
+
7
+ 首包固定为 512 bit,结构如下:
8
+
9
+ | bit 偏移 | bit 长度 | 字段 | 规则 |
10
+ | ---: | ---: | --- | --- |
11
+ | 0 | 24 | 固定标识 | 三个 ASCII 字符 `ADP` |
12
+ | 24 | 16 | 协议版本 | 16-bit 无符号整数,使用大端序,当前版本为 `7` |
13
+ | 40 | 256 | 实例编号 | 当前节点的 `instance_id`,固定32字节 |
14
+ | 296 | 8 | 功能控制区 | bit 0–2为功能开关,bit 3–7为保留位 |
15
+ | 304 | 208 | 全零保留区 | 必须全部为 `0` |
16
+
17
+ 字符串字段统一使用 UTF-8 编码。字段长度不足时在尾部使用 `0x00` 填充,超过字段长度时按字段长度截断。
18
+
19
+ ### 功能控制区
20
+
21
+ 功能控制区固定为8 bit,每个功能占用一个 bit:
22
+
23
+ | bit | 功能 | `0` | `1` | 当前默认值 |
24
+ | ---: | --- | --- | --- | --- |
25
+ | 0 | UDP 主动发现 | 关闭 | 开启 | `1` |
26
+ | 1 | 额外 channel 通道 | 不支持 | 支持 | `1` |
27
+ | 2 | WebSocket 业务端口 | 关闭 | 开启 | `1` |
28
+ | 3–7 | 保留位 | 必须为零 | 不允许 | `0` |
29
+
30
+ 当前默认控制区的二进制内容为:
31
+
32
+ ```text
33
+ 00000111
34
+ ```
35
+
36
+ ### 握手流程
37
+
38
+ 1. TCP 客户端连接 C2C 服务端。
39
+ 2. 客户端发送自己的 512 bit 二进制首包。
40
+ 3. 服务端读取并校验完整首包。
41
+ 4. 服务端校验通过后,向客户端返回自己的 512 bit 二进制首包。
42
+ 5. 客户端校验服务端首包。
43
+ 6. 双方校验均通过后,才开始发送 JSON 业务指令。
44
+
45
+ 服务端会拒绝以下连接:
46
+
47
+ - 首包长度不足 512 bit;
48
+ - 固定标识不是 `ADP`;
49
+ - 目标节点协议版本低于发起连接节点的协议版本;
50
+ - `instance_id` 为空;
51
+ - 字符串字段的补零格式错误;
52
+ - 功能控制区保留位非零;
53
+ - 208 bit 全零保留区存在非零数据。
54
+
55
+ 该首包适用于 C2C 直连、反向连接、多跳转发、额外 channel 通道、外部 `C2CSession` 和本地服务探测。未实现该二进制首包的旧节点无法与新协议节点建立连接,因此升级时需要同时更新同一网络中的全部 C2C 节点。
56
+
57
+ 协议版本字段固定为16 bit,使用大端序表示无符号整数,取值范围为 `0`–`65535`。客户端收到服务端首包后比较版本:目标节点版本大于或等于本机版本时允许连接,目标节点版本低于本机版本时拒绝连接。例如本机版本为 `4`、目标版本为 `5` 时允许连接;本机版本为 `5`、目标版本为 `4` 时拒绝连接。
File without changes
@@ -0,0 +1,6 @@
1
+ from adp_operator.c2c.client import C2CClient
2
+ from adp_operator.c2c.main import main
3
+
4
+
5
+ __all__ = ["C2CClient", "main"]
6
+
@@ -0,0 +1,5 @@
1
+ from adp_operator.c2c.main import main
2
+
3
+
4
+ main()
5
+
@@ -0,0 +1,128 @@
1
+ import hashlib
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import threading
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from adp_operator.c2c.operator_executor import OperatorExecutor
10
+ from adp_operator.c2c.runtime import Service
11
+ from adp_operator.c2c.operator_loader import load_operator
12
+ from adp_operator.c2c.operator import C2COperatorBase
13
+ from adp_operator.c2c.operator_source import prepare_operator_source
14
+
15
+
16
+ class C2CClient(Service):
17
+ """一个进程承载一个算子类,按 info/run 指令加载信息或执行算子。"""
18
+
19
+ def __init__(
20
+ self,
21
+ operator_config: dict,
22
+ node_id: Optional[str] = None,
23
+ network: Optional[str] = None,
24
+ max_concurrent_runs: Optional[int] = None,
25
+ join: Optional[tuple[str, int]] = None,
26
+ ):
27
+ self.operator_config = dict(operator_config)
28
+ self.operator_base = "PermanentOperator" if join is not None else "ADPOperator"
29
+ source = Path(operator_config["module_file"]).expanduser().resolve()
30
+ self._source_config = {**operator_config, "module_file": str(source)}
31
+ self._workspaces = {}
32
+ self._workspace_lock = threading.RLock()
33
+ identity = hashlib.sha256(
34
+ f"{node_id or operator_config['name']}:{operator_config['module_name']}".encode()
35
+ ).hexdigest()
36
+ self._namespace = Path(tempfile.gettempdir()).resolve() / "adp-operator-clients" / identity
37
+ self._namespace.mkdir(parents=True, exist_ok=True, mode=0o700)
38
+ self._temporary_directory = tempfile.TemporaryDirectory(prefix="session-", dir=self._namespace)
39
+ self.working_directory = Path(self._temporary_directory.name)
40
+ self._previous_directory = Path.cwd()
41
+ self._import_root = str(self._previous_directory)
42
+ self._added_import_root = self._import_root not in sys.path
43
+ try:
44
+ target = self.working_directory / source.name
45
+ prepare_operator_source(
46
+ source, target, operator_config["name"], self.operator_base
47
+ )
48
+ self.operator_config["module_file"] = str(target)
49
+ if self._added_import_root:
50
+ sys.path.insert(0, self._import_root)
51
+ os.chdir(self.working_directory)
52
+ self.operator_class = load_operator(self.operator_config)
53
+ if not issubclass(self.operator_class, C2COperatorBase):
54
+ raise TypeError(
55
+ f"{self.operator_class.__name__} 必须继承 ADPOperator 或 PermanentOperator"
56
+ )
57
+ resolved_node_id = node_id or operator_config["name"]
58
+ if max_concurrent_runs is None:
59
+ max_concurrent_runs = max(1, (os.cpu_count() or 1) * 3)
60
+ executor = OperatorExecutor(
61
+ self.operator_class, resolved_node_id, max_concurrent_runs=max_concurrent_runs
62
+ )
63
+ executor.operator_config = self.operator_config
64
+ executor.working_directory = self.working_directory
65
+ executor.get_workspace = self.get_workspace
66
+ executor.close_workspace = self.close_workspace
67
+ super().__init__(resolved_node_id, network, executor, join=join)
68
+ except Exception:
69
+ self._cleanup_workspace()
70
+ raise
71
+
72
+ def _cleanup_workspace(self):
73
+ for session_id in list(self._workspaces):
74
+ self.close_workspace(session_id)
75
+ if Path.cwd() == self.working_directory:
76
+ os.chdir(self._previous_directory)
77
+ if self._added_import_root and self._import_root in sys.path:
78
+ sys.path.remove(self._import_root)
79
+ self._temporary_directory.cleanup()
80
+
81
+ def get_workspace(self, session_id):
82
+ """文件接收和算子运行使用同一个会话目录。"""
83
+ with self._workspace_lock:
84
+ if session_id not in self._workspaces:
85
+ if not self._workspaces and self.working_directory.exists():
86
+ temporary = self._temporary_directory
87
+ else:
88
+ temporary = tempfile.TemporaryDirectory(prefix="session-", dir=self._namespace)
89
+ directory = Path(temporary.name)
90
+ try:
91
+ source = Path(self._source_config["module_file"])
92
+ target = directory / source.name
93
+ prepare_operator_source(
94
+ source, target, self._source_config["name"], self.operator_base
95
+ )
96
+ config = {**self._source_config, "module_file": str(target)}
97
+ self._workspaces[session_id] = (temporary, config)
98
+ except Exception:
99
+ temporary.cleanup()
100
+ raise
101
+ return self._workspaces[session_id][1]
102
+
103
+ def close_workspace(self, session_id):
104
+ with self._workspace_lock:
105
+ workspace = self._workspaces.get(session_id)
106
+ if workspace is None:
107
+ return
108
+ temporary, _ = workspace
109
+ if Path.cwd() == Path(temporary.name):
110
+ os.chdir(self._previous_directory)
111
+ temporary.cleanup()
112
+ del self._workspaces[session_id]
113
+
114
+ def start(self, blocking: bool = True) -> None:
115
+ try:
116
+ super().start(blocking=blocking)
117
+ except Exception:
118
+ if self._stopped:
119
+ self._cleanup_workspace()
120
+ raise
121
+
122
+ def stop(self) -> None:
123
+ super().stop()
124
+ self._cleanup_workspace()
125
+
126
+ def get_extra_info(self):
127
+ return self.executor.get_extra_info()
128
+
@@ -0,0 +1,181 @@
1
+ """与网络入口无关的 C2C 指令实现。"""
2
+
3
+ import base64
4
+ import logging
5
+ import uuid
6
+ from pathlib import Path
7
+
8
+
9
+ def output_event(event, prefix=""):
10
+ text = event.get("message", event)
11
+ lines = str(text).splitlines() or [""]
12
+ if event.get("type") == "log":
13
+ logger = logging.getLogger(event.get("logger") or __name__)
14
+ level = getattr(logging, str(event.get("level", "INFO")).upper(), logging.INFO)
15
+ if not isinstance(level, int):
16
+ level = logging.INFO
17
+ logger.disabled = False
18
+ if logger.getEffectiveLevel() > level:
19
+ logger.setLevel(level)
20
+ if not logger.hasHandlers():
21
+ handler = logging.StreamHandler()
22
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
23
+ logger.addHandler(handler)
24
+ logger.propagate = False
25
+ logger.log(level, "\n".join(f"{prefix}{line}" for line in lines))
26
+ return
27
+ print("\n".join(f"{prefix}{line}" for line in lines), flush=True)
28
+
29
+
30
+ class C2CCommands:
31
+ """统一实现 C2C 的运行、调用和文件指令。"""
32
+
33
+ def __init__(self, transport, session_id: str | None = None) -> None:
34
+ self.transport = transport
35
+ self.session_id = session_id or uuid.uuid4().hex
36
+ self._operators: set[str] = set()
37
+ self._closed = False
38
+
39
+ def can_reach(self, operator_name: str) -> bool:
40
+ return self.transport.can_reach(operator_name, self.session_id)
41
+
42
+ def call(self, operator_name: str, method: str, *args, **kwargs):
43
+ if not self.can_reach(operator_name):
44
+ return None
45
+ result = None
46
+ result_received = False
47
+ for event in self._request(operator_name, {
48
+ "type": "call",
49
+ "operator_name": operator_name,
50
+ "method": method,
51
+ "args": list(args),
52
+ "kwargs": kwargs,
53
+ }, {"call_end", "call_error"}):
54
+ if event.get("type") == "call_error":
55
+ raise RuntimeError(event.get("error", "C2C 方法调用失败"))
56
+ if event.get("type") == "call_result":
57
+ result = event.get("result")
58
+ result_received = True
59
+ if event.get("type") == "call_end":
60
+ if not result_received:
61
+ raise RuntimeError(f"方法 {operator_name}.{method} 未返回结果")
62
+ return result
63
+ raise RuntimeError(f"方法 {operator_name}.{method} 的事件流异常结束")
64
+
65
+ def run(self, message: dict, on_event=None):
66
+ operator_name = message["operator_name"]
67
+ result = None
68
+ for event in self._request(
69
+ operator_name, message, {"run_end", "run_error"}
70
+ ):
71
+ event_type = event.get("type")
72
+ if event_type == "log":
73
+ continue
74
+ if event_type == "run_end" and result is None:
75
+ event = {
76
+ "type": "run_error",
77
+ "status": "error",
78
+ "operator_name": operator_name,
79
+ "pipeline_id": message.get("pipeline_id"),
80
+ "error": f"算子 {operator_name} 运行失败,未返回结果",
81
+ }
82
+ event_type = "run_error"
83
+ if event_type in ("run_result", "run_error"):
84
+ result = event
85
+ if on_event is None:
86
+ output_event(event)
87
+ else:
88
+ on_event(event)
89
+ if event_type == "run_error":
90
+ return result
91
+ if event_type == "run_end":
92
+ return result
93
+ raise RuntimeError(f"算子 {operator_name} 的事件流异常结束")
94
+
95
+ def transfer_file(self, operator_name: str, path, filename: str) -> str:
96
+ offset = 0
97
+ response = None
98
+ with Path(path).open("rb") as source:
99
+ for chunk in iter(lambda: source.read(512 * 1024), b""):
100
+ response = self._single(operator_name, {
101
+ "type": "receive_file",
102
+ "operator_name": operator_name,
103
+ "filename": filename,
104
+ "offset": offset,
105
+ "content": base64.b64encode(chunk).decode("ascii"),
106
+ }, "receive_file_ack")
107
+ offset += len(chunk)
108
+ if response.get("size") != offset:
109
+ raise RuntimeError("文件接收确认不一致")
110
+ if response is None:
111
+ response = self._single(operator_name, {
112
+ "type": "receive_file",
113
+ "operator_name": operator_name,
114
+ "filename": filename,
115
+ "offset": 0,
116
+ "content": "",
117
+ }, "receive_file_ack")
118
+ if response.get("size") != 0:
119
+ raise RuntimeError("空文件接收确认不一致")
120
+ return response["path"]
121
+
122
+ def sync_files(self, operator_name: str, targets: list[str]) -> dict:
123
+ targets = list(dict.fromkeys(targets))
124
+ if not targets:
125
+ return {}
126
+ self._operators.update(targets)
127
+ response = self._single(operator_name, {
128
+ "type": "sync_files",
129
+ "operator_name": operator_name,
130
+ "targets": targets,
131
+ }, "sync_files_ack")
132
+ return response["synchronized"]
133
+
134
+ def close(self) -> None:
135
+ if self._closed:
136
+ return
137
+ error = None
138
+ try:
139
+ for operator_name in self._operators:
140
+ try:
141
+ self._single(operator_name, {
142
+ "type": "close_session",
143
+ "operator_name": operator_name,
144
+ }, "close_session_ack")
145
+ except Exception as exc:
146
+ error = error or exc
147
+ finally:
148
+ self._closed = True
149
+ self.transport.close()
150
+ if error is not None:
151
+ raise error
152
+
153
+ def _single(self, operator_name: str, message: dict, response_type: str) -> dict:
154
+ response = None
155
+ for event in self._request(operator_name, message, {response_type}):
156
+ response = event
157
+ if response is None or response.get("type") != response_type:
158
+ raise RuntimeError(f"{message['type']} 响应无效")
159
+ if response.get("status") == "error":
160
+ raise RuntimeError(response.get("error", "C2C 请求失败"))
161
+ return response
162
+
163
+ def _request(
164
+ self,
165
+ operator_name: str,
166
+ message: dict,
167
+ terminal_types: set[str],
168
+ ):
169
+ self._operators.add(operator_name)
170
+ yield from self.transport.request(
171
+ operator_name,
172
+ message,
173
+ self.session_id,
174
+ terminal_types,
175
+ )
176
+
177
+ def __enter__(self):
178
+ return self
179
+
180
+ def __exit__(self, exc_type, exc_value, traceback):
181
+ self.close()
@@ -0,0 +1,11 @@
1
+ BROADCAST_BASE_PORT = 50000
2
+ BROADCAST_PORT_RANGE = 1000
3
+ SERVICE_BASE_PORT = 60000
4
+ SERVICE_PORT_RANGE = 1000
5
+ WEBSOCKET_BASE_PORT = 61000
6
+ WEBSOCKET_PORT_RANGE = 1000
7
+ BROADCAST_INTERVAL = 3
8
+ PEER_SYNC_INTERVAL = 3
9
+ PEER_TTL = 300
10
+ MAX_REQUEST_SIZE = 1024 * 1024
11
+