ls-algorithm-plugin-sdk 0.3.0__tar.gz → 0.3.1__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.
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/PKG-INFO +6 -17
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/README.md +5 -16
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/pyproject.toml +1 -1
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/configure.py +55 -42
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/deployment.py +2 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/ls_algorithm_plugin_sdk.egg-info/PKG-INFO +6 -17
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_configure.py +70 -36
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_deployment.py +38 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/setup.cfg +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/parsing.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/run.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/serve.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/context.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/errors.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/examples/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/examples/example_algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/examples/simulated_algorithm.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/loader.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/models.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/registration.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/release.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/runner.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/service.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui/__init__.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui/app.css +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui/app.js +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui/index.html +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui_app.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/ls_algorithm_plugin_sdk.egg-info/SOURCES.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/ls_algorithm_plugin_sdk.egg-info/dependency_links.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/ls_algorithm_plugin_sdk.egg-info/entry_points.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/ls_algorithm_plugin_sdk.egg-info/requires.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/ls_algorithm_plugin_sdk.egg-info/top_level.txt +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_cli.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_models.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_registration.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_release.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_runner.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_sdk_automation.py +0 -0
- {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_service.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ls-algorithm-plugin-sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Protocol-independent runtime SDK for dataset algorithms
|
|
5
5
|
Author: Ling Robotics
|
|
6
6
|
License: Proprietary
|
|
@@ -15,10 +15,6 @@ Requires-Dist: build>=1.2; extra == "dev"
|
|
|
15
15
|
|
|
16
16
|
# Algorithm Plugin SDK
|
|
17
17
|
|
|
18
|
-
算法仓库只实现一个 `Algorithm` 类并声明 entry point。SDK 统一提供单次运行、
|
|
19
|
-
WebUI、HTTP service、release manifest、compute 注册和心跳。算法仓库不需要
|
|
20
|
-
Dockerfile 或 Web 框架代码。
|
|
21
|
-
|
|
22
18
|
## 算法侧最小接口
|
|
23
19
|
|
|
24
20
|
```python
|
|
@@ -30,8 +26,7 @@ class MyAlgorithm(Algorithm):
|
|
|
30
26
|
def execute(self, request, context): ...
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point
|
|
34
|
-
会被所有运行方式复用。
|
|
29
|
+
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point。
|
|
35
30
|
|
|
36
31
|
## 单次运行
|
|
37
32
|
|
|
@@ -49,10 +44,9 @@ algorithm-plugin run camera-space-mano \
|
|
|
49
44
|
|
|
50
45
|
目标算法仓库必须满足:
|
|
51
46
|
|
|
52
|
-
- 仓库本身是 Git
|
|
47
|
+
- 仓库本身是 Git 工作树根目录。
|
|
53
48
|
- 仓库包含自己的 `.venv`。
|
|
54
49
|
- `<repository>/.venv/bin/algorithm-plugin` 存在且可执行。
|
|
55
|
-
- 该启动器的 `run --help` 和 `serve --help` 均可正常加载。
|
|
56
50
|
|
|
57
51
|
建议将生成的 `release-manifest.json` 和 `algorithm-plugin.json` 加入算法仓库的
|
|
58
52
|
`.gitignore`,不要手动编辑或提交它们。
|
|
@@ -64,16 +58,11 @@ sudo /srv/camera-space-mano/.venv/bin/algorithm-plugin configure \
|
|
|
64
58
|
--repository /srv/camera-space-mano \
|
|
65
59
|
--compute-url http://compute:5180 \
|
|
66
60
|
--api-key "$LDP_INTERNAL_API_KEY" \
|
|
67
|
-
--
|
|
68
|
-
--input-root /data/jobs/input \
|
|
69
|
-
--workspace-root /data/jobs/workspace \
|
|
61
|
+
--port 9030 \
|
|
70
62
|
--cluster production \
|
|
71
|
-
--namespace camera-space-mano \
|
|
72
|
-
--node-name 10.0.0.4 \
|
|
73
63
|
--gpu-ids 0,1
|
|
74
64
|
```
|
|
75
65
|
|
|
76
|
-
service name 默认使用仓库目录名,也可以通过 `--service-name` 指定。
|
|
77
66
|
重复执行 configure 需要显式提供 `--force`。
|
|
78
67
|
|
|
79
68
|
确认输出后执行:
|
|
@@ -82,7 +71,7 @@ service name 默认使用仓库目录名,也可以通过 `--service-name` 指
|
|
|
82
71
|
sudo systemctl daemon-reload
|
|
83
72
|
sudo systemctl enable camera-space-mano.service
|
|
84
73
|
sudo systemctl restart camera-space-mano.service
|
|
85
|
-
sudo systemctl status camera-space-mano.service
|
|
74
|
+
sudo systemctl status camera-space-mano.service --no-pager
|
|
86
75
|
```
|
|
87
76
|
|
|
88
77
|
## serve
|
|
@@ -95,6 +84,6 @@ sudo systemctl status camera-space-mano.service
|
|
|
95
84
|
## 安装与测试
|
|
96
85
|
|
|
97
86
|
```bash
|
|
98
|
-
python -m pip install
|
|
87
|
+
python -m pip install .
|
|
99
88
|
python -m unittest discover -s tests -v
|
|
100
89
|
```
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
# Algorithm Plugin SDK
|
|
2
2
|
|
|
3
|
-
算法仓库只实现一个 `Algorithm` 类并声明 entry point。SDK 统一提供单次运行、
|
|
4
|
-
WebUI、HTTP service、release manifest、compute 注册和心跳。算法仓库不需要
|
|
5
|
-
Dockerfile 或 Web 框架代码。
|
|
6
|
-
|
|
7
3
|
## 算法侧最小接口
|
|
8
4
|
|
|
9
5
|
```python
|
|
@@ -15,8 +11,7 @@ class MyAlgorithm(Algorithm):
|
|
|
15
11
|
def execute(self, request, context): ...
|
|
16
12
|
```
|
|
17
13
|
|
|
18
|
-
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point
|
|
19
|
-
会被所有运行方式复用。
|
|
14
|
+
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point。
|
|
20
15
|
|
|
21
16
|
## 单次运行
|
|
22
17
|
|
|
@@ -34,10 +29,9 @@ algorithm-plugin run camera-space-mano \
|
|
|
34
29
|
|
|
35
30
|
目标算法仓库必须满足:
|
|
36
31
|
|
|
37
|
-
- 仓库本身是 Git
|
|
32
|
+
- 仓库本身是 Git 工作树根目录。
|
|
38
33
|
- 仓库包含自己的 `.venv`。
|
|
39
34
|
- `<repository>/.venv/bin/algorithm-plugin` 存在且可执行。
|
|
40
|
-
- 该启动器的 `run --help` 和 `serve --help` 均可正常加载。
|
|
41
35
|
|
|
42
36
|
建议将生成的 `release-manifest.json` 和 `algorithm-plugin.json` 加入算法仓库的
|
|
43
37
|
`.gitignore`,不要手动编辑或提交它们。
|
|
@@ -49,16 +43,11 @@ sudo /srv/camera-space-mano/.venv/bin/algorithm-plugin configure \
|
|
|
49
43
|
--repository /srv/camera-space-mano \
|
|
50
44
|
--compute-url http://compute:5180 \
|
|
51
45
|
--api-key "$LDP_INTERNAL_API_KEY" \
|
|
52
|
-
--
|
|
53
|
-
--input-root /data/jobs/input \
|
|
54
|
-
--workspace-root /data/jobs/workspace \
|
|
46
|
+
--port 9030 \
|
|
55
47
|
--cluster production \
|
|
56
|
-
--namespace camera-space-mano \
|
|
57
|
-
--node-name 10.0.0.4 \
|
|
58
48
|
--gpu-ids 0,1
|
|
59
49
|
```
|
|
60
50
|
|
|
61
|
-
service name 默认使用仓库目录名,也可以通过 `--service-name` 指定。
|
|
62
51
|
重复执行 configure 需要显式提供 `--force`。
|
|
63
52
|
|
|
64
53
|
确认输出后执行:
|
|
@@ -67,7 +56,7 @@ service name 默认使用仓库目录名,也可以通过 `--service-name` 指
|
|
|
67
56
|
sudo systemctl daemon-reload
|
|
68
57
|
sudo systemctl enable camera-space-mano.service
|
|
69
58
|
sudo systemctl restart camera-space-mano.service
|
|
70
|
-
sudo systemctl status camera-space-mano.service
|
|
59
|
+
sudo systemctl status camera-space-mano.service --no-pager
|
|
71
60
|
```
|
|
72
61
|
|
|
73
62
|
## serve
|
|
@@ -80,6 +69,6 @@ sudo systemctl status camera-space-mano.service
|
|
|
80
69
|
## 安装与测试
|
|
81
70
|
|
|
82
71
|
```bash
|
|
83
|
-
python -m pip install
|
|
72
|
+
python -m pip install .
|
|
84
73
|
python -m unittest discover -s tests -v
|
|
85
74
|
```
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import ipaddress
|
|
4
5
|
import os
|
|
5
6
|
import re
|
|
7
|
+
import socket
|
|
6
8
|
import subprocess
|
|
7
9
|
import sys
|
|
8
10
|
from pathlib import Path
|
|
11
|
+
from urllib.parse import urlparse
|
|
9
12
|
|
|
10
13
|
from ..deployment import (
|
|
11
14
|
DEFAULT_CONFIG_NAME,
|
|
@@ -17,6 +20,11 @@ from .parsing import environment, gpu_ids
|
|
|
17
20
|
|
|
18
21
|
|
|
19
22
|
SYSTEMD_UNIT_DIR = Path("/etc/systemd/system")
|
|
23
|
+
INPUT_ROOT = Path("/mnt/ldp_uploads/ldp-uploads")
|
|
24
|
+
PRIVATE_IPV4_NETWORKS = tuple(
|
|
25
|
+
ipaddress.ip_network(network)
|
|
26
|
+
for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
|
|
27
|
+
)
|
|
20
28
|
|
|
21
29
|
SERVICE_NAME_PATTERN = re.compile(
|
|
22
30
|
r"^[A-Za-z0-9][A-Za-z0-9_.@-]*$"
|
|
@@ -65,18 +73,6 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
65
73
|
"--api-key",
|
|
66
74
|
default=os.getenv("LDP_INTERNAL_API_KEY"),
|
|
67
75
|
)
|
|
68
|
-
parser.add_argument(
|
|
69
|
-
"--public-url",
|
|
70
|
-
default=os.getenv("LDP_PLUGIN_PUBLIC_URL"),
|
|
71
|
-
)
|
|
72
|
-
parser.add_argument(
|
|
73
|
-
"--input-root",
|
|
74
|
-
default=os.getenv("LDP_JOB_INPUT_ROOT"),
|
|
75
|
-
)
|
|
76
|
-
parser.add_argument(
|
|
77
|
-
"--workspace-root",
|
|
78
|
-
default=os.getenv("LDP_JOB_WORKSPACE_ROOT"),
|
|
79
|
-
)
|
|
80
76
|
parser.add_argument(
|
|
81
77
|
"--instance-key",
|
|
82
78
|
default=os.getenv("LDP_PLUGIN_INSTANCE_KEY"),
|
|
@@ -85,14 +81,6 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
85
81
|
"--cluster",
|
|
86
82
|
default=os.getenv("LDP_CLUSTER"),
|
|
87
83
|
)
|
|
88
|
-
parser.add_argument(
|
|
89
|
-
"--namespace",
|
|
90
|
-
default=os.getenv("POD_NAMESPACE"),
|
|
91
|
-
)
|
|
92
|
-
parser.add_argument(
|
|
93
|
-
"--node-name",
|
|
94
|
-
default=os.getenv("NODE_NAME"),
|
|
95
|
-
)
|
|
96
84
|
parser.add_argument(
|
|
97
85
|
"--local",
|
|
98
86
|
action="store_true",
|
|
@@ -105,10 +93,6 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
105
93
|
default=[],
|
|
106
94
|
metavar="NAME=VALUE",
|
|
107
95
|
)
|
|
108
|
-
parser.add_argument(
|
|
109
|
-
"--service-name",
|
|
110
|
-
help="systemd unit name without .service; defaults to repository name",
|
|
111
|
-
)
|
|
112
96
|
parser.add_argument(
|
|
113
97
|
"--force",
|
|
114
98
|
action="store_true",
|
|
@@ -119,33 +103,35 @@ def configure_parser(parser: argparse.ArgumentParser) -> None:
|
|
|
119
103
|
|
|
120
104
|
def resolve_service_name(
|
|
121
105
|
repository: Path,
|
|
122
|
-
requested: str | None,
|
|
123
106
|
) -> str:
|
|
124
|
-
name =
|
|
107
|
+
name = repository.name
|
|
125
108
|
if name.endswith(".service"):
|
|
126
109
|
raise ValueError(
|
|
127
|
-
"
|
|
110
|
+
"repository name must not include the .service suffix"
|
|
128
111
|
)
|
|
129
112
|
if not SERVICE_NAME_PATTERN.fullmatch(name):
|
|
130
113
|
raise ValueError(
|
|
131
|
-
"
|
|
114
|
+
"repository name may contain only letters, numbers, "
|
|
132
115
|
"underscore, dot, @, and hyphen"
|
|
133
116
|
)
|
|
134
117
|
return name
|
|
135
118
|
|
|
136
119
|
|
|
137
|
-
def
|
|
120
|
+
def _escape_systemd_path(value: Path) -> str:
|
|
138
121
|
text = str(value)
|
|
139
122
|
if "\n" in text or "\r" in text or "\0" in text:
|
|
140
123
|
raise ValueError(
|
|
141
124
|
f"systemd path contains control characters: {value}"
|
|
142
125
|
)
|
|
143
|
-
|
|
126
|
+
return (
|
|
144
127
|
text.replace("\\", "\\\\")
|
|
145
128
|
.replace('"', '\\"')
|
|
146
129
|
.replace("%", "%%")
|
|
147
130
|
)
|
|
148
|
-
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _quote_systemd_path(value: Path) -> str:
|
|
134
|
+
return f'"{_escape_systemd_path(value)}"'
|
|
149
135
|
|
|
150
136
|
|
|
151
137
|
def render_systemd_unit(
|
|
@@ -167,7 +153,7 @@ def render_systemd_unit(
|
|
|
167
153
|
"[Service]",
|
|
168
154
|
"Type=simple",
|
|
169
155
|
"User=root",
|
|
170
|
-
f"WorkingDirectory={
|
|
156
|
+
f"WorkingDirectory={_escape_systemd_path(root)}",
|
|
171
157
|
(
|
|
172
158
|
f"ExecStart={_quote_systemd_path(launcher)} "
|
|
173
159
|
f"serve --config {_quote_systemd_path(config)}"
|
|
@@ -317,6 +303,35 @@ def resolve_config_path(
|
|
|
317
303
|
return path.resolve()
|
|
318
304
|
|
|
319
305
|
|
|
306
|
+
def resolve_private_ipv4(compute_url: str) -> str:
|
|
307
|
+
parsed = urlparse(compute_url)
|
|
308
|
+
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
|
|
309
|
+
raise ValueError("compute URL must be an http or https URL")
|
|
310
|
+
try:
|
|
311
|
+
port = parsed.port or (443 if parsed.scheme == "https" else 80)
|
|
312
|
+
except ValueError as exc:
|
|
313
|
+
raise ValueError("compute URL has an invalid port") from exc
|
|
314
|
+
|
|
315
|
+
try:
|
|
316
|
+
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as probe:
|
|
317
|
+
probe.connect((parsed.hostname, port))
|
|
318
|
+
value = probe.getsockname()[0]
|
|
319
|
+
except OSError as exc:
|
|
320
|
+
raise RuntimeError(
|
|
321
|
+
f"cannot resolve the local IP used to reach {parsed.hostname}: {exc}"
|
|
322
|
+
) from exc
|
|
323
|
+
|
|
324
|
+
address = ipaddress.ip_address(value)
|
|
325
|
+
if not isinstance(address, ipaddress.IPv4Address) or not any(
|
|
326
|
+
address in network for network in PRIVATE_IPV4_NETWORKS
|
|
327
|
+
):
|
|
328
|
+
raise RuntimeError(
|
|
329
|
+
f"the local IP used to reach {parsed.hostname} is not a private IPv4: "
|
|
330
|
+
f"{address}"
|
|
331
|
+
)
|
|
332
|
+
return str(address)
|
|
333
|
+
|
|
334
|
+
|
|
320
335
|
def registration_from_args(
|
|
321
336
|
args: argparse.Namespace,
|
|
322
337
|
repository: Path,
|
|
@@ -327,12 +342,9 @@ def registration_from_args(
|
|
|
327
342
|
supplied = {
|
|
328
343
|
"computeUrl": args.compute_url,
|
|
329
344
|
"apiKey": args.api_key,
|
|
330
|
-
"
|
|
331
|
-
"
|
|
332
|
-
"workspaceRoot": args.workspace_root,
|
|
345
|
+
"inputRoot": str(INPUT_ROOT),
|
|
346
|
+
"workspaceRoot": str(repository.resolve()),
|
|
333
347
|
"cluster": args.cluster,
|
|
334
|
-
"namespace": args.namespace,
|
|
335
|
-
"nodeName": args.node_name,
|
|
336
348
|
}
|
|
337
349
|
missing = [
|
|
338
350
|
name
|
|
@@ -350,6 +362,9 @@ def registration_from_args(
|
|
|
350
362
|
name: str(value)
|
|
351
363
|
for name, value in supplied.items()
|
|
352
364
|
}
|
|
365
|
+
private_ipv4 = resolve_private_ipv4(registration["computeUrl"])
|
|
366
|
+
registration["publicUrl"] = f"http://{private_ipv4}:{args.port}"
|
|
367
|
+
registration["nodeName"] = private_ipv4
|
|
353
368
|
registration["instanceKey"] = stable_instance_key(
|
|
354
369
|
repository / ".algorithm-plugin" / "instance-key",
|
|
355
370
|
requested=args.instance_key,
|
|
@@ -383,10 +398,7 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
383
398
|
require_root()
|
|
384
399
|
repository = validate_repository(args.repository)
|
|
385
400
|
validate_configure_runtime(repository)
|
|
386
|
-
service_name = resolve_service_name(
|
|
387
|
-
repository,
|
|
388
|
-
args.service_name,
|
|
389
|
-
)
|
|
401
|
+
service_name = resolve_service_name(repository)
|
|
390
402
|
config_path = resolve_config_path(
|
|
391
403
|
repository,
|
|
392
404
|
args.config_output,
|
|
@@ -418,4 +430,5 @@ def execute(args: argparse.Namespace) -> int:
|
|
|
418
430
|
print(" sudo systemctl daemon-reload")
|
|
419
431
|
print(f" sudo systemctl enable {service_name}.service")
|
|
420
432
|
print(f" sudo systemctl restart {service_name}.service")
|
|
421
|
-
|
|
433
|
+
print(f" sudo systemctl status {service_name}.service --no-pager")
|
|
434
|
+
return 0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ls-algorithm-plugin-sdk
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Protocol-independent runtime SDK for dataset algorithms
|
|
5
5
|
Author: Ling Robotics
|
|
6
6
|
License: Proprietary
|
|
@@ -15,10 +15,6 @@ Requires-Dist: build>=1.2; extra == "dev"
|
|
|
15
15
|
|
|
16
16
|
# Algorithm Plugin SDK
|
|
17
17
|
|
|
18
|
-
算法仓库只实现一个 `Algorithm` 类并声明 entry point。SDK 统一提供单次运行、
|
|
19
|
-
WebUI、HTTP service、release manifest、compute 注册和心跳。算法仓库不需要
|
|
20
|
-
Dockerfile 或 Web 框架代码。
|
|
21
|
-
|
|
22
18
|
## 算法侧最小接口
|
|
23
19
|
|
|
24
20
|
```python
|
|
@@ -30,8 +26,7 @@ class MyAlgorithm(Algorithm):
|
|
|
30
26
|
def execute(self, request, context): ...
|
|
31
27
|
```
|
|
32
28
|
|
|
33
|
-
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point
|
|
34
|
-
会被所有运行方式复用。
|
|
29
|
+
算法包需要声明 `algorithm_plugin_sdk.algorithms` entry point。
|
|
35
30
|
|
|
36
31
|
## 单次运行
|
|
37
32
|
|
|
@@ -49,10 +44,9 @@ algorithm-plugin run camera-space-mano \
|
|
|
49
44
|
|
|
50
45
|
目标算法仓库必须满足:
|
|
51
46
|
|
|
52
|
-
- 仓库本身是 Git
|
|
47
|
+
- 仓库本身是 Git 工作树根目录。
|
|
53
48
|
- 仓库包含自己的 `.venv`。
|
|
54
49
|
- `<repository>/.venv/bin/algorithm-plugin` 存在且可执行。
|
|
55
|
-
- 该启动器的 `run --help` 和 `serve --help` 均可正常加载。
|
|
56
50
|
|
|
57
51
|
建议将生成的 `release-manifest.json` 和 `algorithm-plugin.json` 加入算法仓库的
|
|
58
52
|
`.gitignore`,不要手动编辑或提交它们。
|
|
@@ -64,16 +58,11 @@ sudo /srv/camera-space-mano/.venv/bin/algorithm-plugin configure \
|
|
|
64
58
|
--repository /srv/camera-space-mano \
|
|
65
59
|
--compute-url http://compute:5180 \
|
|
66
60
|
--api-key "$LDP_INTERNAL_API_KEY" \
|
|
67
|
-
--
|
|
68
|
-
--input-root /data/jobs/input \
|
|
69
|
-
--workspace-root /data/jobs/workspace \
|
|
61
|
+
--port 9030 \
|
|
70
62
|
--cluster production \
|
|
71
|
-
--namespace camera-space-mano \
|
|
72
|
-
--node-name 10.0.0.4 \
|
|
73
63
|
--gpu-ids 0,1
|
|
74
64
|
```
|
|
75
65
|
|
|
76
|
-
service name 默认使用仓库目录名,也可以通过 `--service-name` 指定。
|
|
77
66
|
重复执行 configure 需要显式提供 `--force`。
|
|
78
67
|
|
|
79
68
|
确认输出后执行:
|
|
@@ -82,7 +71,7 @@ service name 默认使用仓库目录名,也可以通过 `--service-name` 指
|
|
|
82
71
|
sudo systemctl daemon-reload
|
|
83
72
|
sudo systemctl enable camera-space-mano.service
|
|
84
73
|
sudo systemctl restart camera-space-mano.service
|
|
85
|
-
sudo systemctl status camera-space-mano.service
|
|
74
|
+
sudo systemctl status camera-space-mano.service --no-pager
|
|
86
75
|
```
|
|
87
76
|
|
|
88
77
|
## serve
|
|
@@ -95,6 +84,6 @@ sudo systemctl status camera-space-mano.service
|
|
|
95
84
|
## 安装与测试
|
|
96
85
|
|
|
97
86
|
```bash
|
|
98
|
-
python -m pip install
|
|
87
|
+
python -m pip install .
|
|
99
88
|
python -m unittest discover -s tests -v
|
|
100
89
|
```
|
|
@@ -14,6 +14,7 @@ from algorithm_plugin_sdk.cli_impl.configure import (
|
|
|
14
14
|
generate_config,
|
|
15
15
|
registration_from_args,
|
|
16
16
|
render_systemd_unit,
|
|
17
|
+
resolve_private_ipv4,
|
|
17
18
|
resolve_service_name,
|
|
18
19
|
validate_configure_runtime,
|
|
19
20
|
validate_repository,
|
|
@@ -142,7 +143,6 @@ class ConfigureTests(unittest.TestCase):
|
|
|
142
143
|
"--gpu-ids", "2,0",
|
|
143
144
|
"--service-token", "secret",
|
|
144
145
|
"--env", "DEMO_MODE=fast",
|
|
145
|
-
"--service-name", "demo-service",
|
|
146
146
|
"--force",
|
|
147
147
|
]
|
|
148
148
|
)
|
|
@@ -157,9 +157,22 @@ class ConfigureTests(unittest.TestCase):
|
|
|
157
157
|
self.assertEqual(args.gpu_ids, [2, 0])
|
|
158
158
|
self.assertEqual(args.service_token, "secret")
|
|
159
159
|
self.assertEqual(args.env, [("DEMO_MODE", "fast")])
|
|
160
|
-
self.assertEqual(args.service_name, "demo-service")
|
|
161
160
|
self.assertTrue(args.force)
|
|
162
161
|
|
|
162
|
+
def test_rejects_derived_configure_options(self) -> None:
|
|
163
|
+
for option in (
|
|
164
|
+
"--input-root",
|
|
165
|
+
"--workspace-root",
|
|
166
|
+
"--public-url",
|
|
167
|
+
"--namespace",
|
|
168
|
+
"--node-name",
|
|
169
|
+
"--service-name",
|
|
170
|
+
):
|
|
171
|
+
with self.subTest(option=option), self.assertRaises(SystemExit):
|
|
172
|
+
build_parser().parse_args(
|
|
173
|
+
["configure", option, "custom-value"]
|
|
174
|
+
)
|
|
175
|
+
|
|
163
176
|
def test_generates_and_saves_config_for_the_existing_repository(self) -> None:
|
|
164
177
|
repository = Path("/srv/demo").resolve()
|
|
165
178
|
args = build_parser().parse_args(
|
|
@@ -202,17 +215,13 @@ class ConfigureTests(unittest.TestCase):
|
|
|
202
215
|
|
|
203
216
|
with self.assertRaisesRegex(
|
|
204
217
|
ValueError,
|
|
205
|
-
"computeUrl.*apiKey.*
|
|
218
|
+
"computeUrl.*apiKey.*cluster",
|
|
206
219
|
):
|
|
207
220
|
registration_from_args(args, Path("/srv/demo"))
|
|
208
221
|
|
|
209
222
|
def test_builds_registration_and_reuses_instance_key(self) -> None:
|
|
210
223
|
with tempfile.TemporaryDirectory() as directory:
|
|
211
224
|
repository = Path(directory)
|
|
212
|
-
input_root = repository / "input"
|
|
213
|
-
workspace_root = repository / "workspace"
|
|
214
|
-
input_root.mkdir()
|
|
215
|
-
workspace_root.mkdir()
|
|
216
225
|
args = build_parser().parse_args(
|
|
217
226
|
[
|
|
218
227
|
"configure",
|
|
@@ -220,26 +229,30 @@ class ConfigureTests(unittest.TestCase):
|
|
|
220
229
|
"http://compute:5180",
|
|
221
230
|
"--api-key",
|
|
222
231
|
"secret",
|
|
223
|
-
"--public-url",
|
|
224
|
-
"http://127.0.0.1:9000",
|
|
225
|
-
"--input-root",
|
|
226
|
-
str(input_root),
|
|
227
|
-
"--workspace-root",
|
|
228
|
-
str(workspace_root),
|
|
229
232
|
"--cluster",
|
|
230
233
|
"production",
|
|
231
|
-
"--
|
|
232
|
-
"
|
|
233
|
-
"--node-name",
|
|
234
|
-
"node-1",
|
|
234
|
+
"--port",
|
|
235
|
+
"9030",
|
|
235
236
|
]
|
|
236
237
|
)
|
|
237
238
|
|
|
238
|
-
|
|
239
|
-
|
|
239
|
+
with patch(
|
|
240
|
+
"algorithm_plugin_sdk.cli_impl.configure.resolve_private_ipv4",
|
|
241
|
+
return_value="172.21.32.10",
|
|
242
|
+
):
|
|
243
|
+
first = registration_from_args(args, repository)
|
|
244
|
+
second = registration_from_args(args, repository)
|
|
240
245
|
|
|
241
246
|
self.assertEqual(first, second)
|
|
242
247
|
self.assertEqual(first["computeUrl"], "http://compute:5180")
|
|
248
|
+
self.assertEqual(first["nodeName"], "172.21.32.10")
|
|
249
|
+
self.assertEqual(first["publicUrl"], "http://172.21.32.10:9030")
|
|
250
|
+
self.assertEqual(
|
|
251
|
+
first["inputRoot"],
|
|
252
|
+
"/mnt/ldp_uploads/ldp-uploads",
|
|
253
|
+
)
|
|
254
|
+
self.assertEqual(first["workspaceRoot"], str(repository.resolve()))
|
|
255
|
+
self.assertNotIn("namespace", first)
|
|
243
256
|
self.assertTrue(first["instanceKey"])
|
|
244
257
|
self.assertEqual(
|
|
245
258
|
(
|
|
@@ -250,24 +263,48 @@ class ConfigureTests(unittest.TestCase):
|
|
|
250
263
|
first["instanceKey"],
|
|
251
264
|
)
|
|
252
265
|
|
|
266
|
+
def test_resolves_private_ipv4_from_compute_route(self) -> None:
|
|
267
|
+
probe = Mock()
|
|
268
|
+
probe.__enter__ = Mock(return_value=probe)
|
|
269
|
+
probe.__exit__ = Mock(return_value=None)
|
|
270
|
+
probe.getsockname.return_value = ("172.21.32.10", 42000)
|
|
271
|
+
|
|
272
|
+
with patch(
|
|
273
|
+
"algorithm_plugin_sdk.cli_impl.configure.socket.socket",
|
|
274
|
+
return_value=probe,
|
|
275
|
+
):
|
|
276
|
+
actual = resolve_private_ipv4("http://compute.internal:5180")
|
|
277
|
+
|
|
278
|
+
self.assertEqual(actual, "172.21.32.10")
|
|
279
|
+
probe.connect.assert_called_once_with(("compute.internal", 5180))
|
|
280
|
+
|
|
281
|
+
def test_rejects_non_private_route_ipv4(self) -> None:
|
|
282
|
+
probe = Mock()
|
|
283
|
+
probe.__enter__ = Mock(return_value=probe)
|
|
284
|
+
probe.__exit__ = Mock(return_value=None)
|
|
285
|
+
probe.getsockname.return_value = ("62.234.57.176", 42000)
|
|
286
|
+
|
|
287
|
+
with (
|
|
288
|
+
patch(
|
|
289
|
+
"algorithm_plugin_sdk.cli_impl.configure.socket.socket",
|
|
290
|
+
return_value=probe,
|
|
291
|
+
),
|
|
292
|
+
self.assertRaisesRegex(RuntimeError, "not a private IPv4"),
|
|
293
|
+
):
|
|
294
|
+
resolve_private_ipv4("https://compute.example.com")
|
|
295
|
+
|
|
253
296
|
def test_defaults_service_name_to_repository_directory(self) -> None:
|
|
254
297
|
repository = Path("/srv/demo-algorithm")
|
|
255
298
|
|
|
256
299
|
self.assertEqual(
|
|
257
|
-
resolve_service_name(repository
|
|
300
|
+
resolve_service_name(repository),
|
|
258
301
|
"demo-algorithm",
|
|
259
302
|
)
|
|
260
|
-
self.assertEqual(
|
|
261
|
-
resolve_service_name(repository, "custom-name"),
|
|
262
|
-
"custom-name",
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
def test_rejects_unsafe_or_suffixed_service_names(self) -> None:
|
|
266
|
-
repository = Path("/srv/demo")
|
|
267
303
|
|
|
268
|
-
|
|
304
|
+
def test_rejects_unsafe_repository_names(self) -> None:
|
|
305
|
+
for name in ("bad name", "..", "demo.service"):
|
|
269
306
|
with self.subTest(name=name), self.assertRaises(ValueError):
|
|
270
|
-
resolve_service_name(
|
|
307
|
+
resolve_service_name(Path("/srv") / name)
|
|
271
308
|
|
|
272
309
|
def test_renders_root_service_from_repository_virtualenv(self) -> None:
|
|
273
310
|
unit = render_systemd_unit(
|
|
@@ -286,7 +323,7 @@ class ConfigureTests(unittest.TestCase):
|
|
|
286
323
|
"[Service]\n"
|
|
287
324
|
"Type=simple\n"
|
|
288
325
|
"User=root\n"
|
|
289
|
-
|
|
326
|
+
"WorkingDirectory=/srv/demo algorithm\n"
|
|
290
327
|
'ExecStart="/srv/demo algorithm/.venv/bin/algorithm-plugin" '
|
|
291
328
|
'serve --config "/srv/demo algorithm/algorithm-plugin.json"\n'
|
|
292
329
|
"Environment=PYTHONUNBUFFERED=1\n"
|
|
@@ -432,8 +469,6 @@ class ConfigureTests(unittest.TestCase):
|
|
|
432
469
|
"configure",
|
|
433
470
|
"--repository",
|
|
434
471
|
str(repository),
|
|
435
|
-
"--service-name",
|
|
436
|
-
"demo",
|
|
437
472
|
"--local",
|
|
438
473
|
]
|
|
439
474
|
)
|
|
@@ -483,7 +518,8 @@ class ConfigureTests(unittest.TestCase):
|
|
|
483
518
|
"Next commands:\n"
|
|
484
519
|
" sudo systemctl daemon-reload\n"
|
|
485
520
|
" sudo systemctl enable demo.service\n"
|
|
486
|
-
" sudo systemctl restart demo.service\n"
|
|
521
|
+
" sudo systemctl restart demo.service\n"
|
|
522
|
+
" sudo systemctl status demo.service --no-pager\n",
|
|
487
523
|
)
|
|
488
524
|
|
|
489
525
|
def test_execute_rejects_unit_conflict_before_config_generation(self) -> None:
|
|
@@ -493,8 +529,6 @@ class ConfigureTests(unittest.TestCase):
|
|
|
493
529
|
"configure",
|
|
494
530
|
"--repository",
|
|
495
531
|
str(repository),
|
|
496
|
-
"--service-name",
|
|
497
|
-
"demo",
|
|
498
532
|
"--local",
|
|
499
533
|
]
|
|
500
534
|
)
|
|
@@ -529,4 +563,4 @@ class ConfigureTests(unittest.TestCase):
|
|
|
529
563
|
|
|
530
564
|
|
|
531
565
|
if __name__ == "__main__":
|
|
532
|
-
unittest.main()
|
|
566
|
+
unittest.main()
|
|
@@ -95,6 +95,44 @@ class DeploymentConfigTests(unittest.TestCase):
|
|
|
95
95
|
)
|
|
96
96
|
self.assertEqual(stat.S_IMODE(config.path.stat().st_mode), 0o600)
|
|
97
97
|
|
|
98
|
+
def test_uses_metadata_name_as_registration_namespace(self) -> None:
|
|
99
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
100
|
+
root = Path(directory)
|
|
101
|
+
self._repository(root)
|
|
102
|
+
registration = {
|
|
103
|
+
"computeUrl": "http://compute:5180",
|
|
104
|
+
"apiKey": "secret",
|
|
105
|
+
"publicUrl": "http://127.0.0.1:9000",
|
|
106
|
+
"inputRoot": str(root),
|
|
107
|
+
"workspaceRoot": str(root),
|
|
108
|
+
"cluster": "production",
|
|
109
|
+
"namespace": "ignored",
|
|
110
|
+
"nodeName": "node-1",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
with patch(
|
|
114
|
+
"algorithm_plugin_sdk.deployment.load_algorithm",
|
|
115
|
+
return_value=FakeAlgorithm(),
|
|
116
|
+
):
|
|
117
|
+
config = DeploymentConfig.generate(
|
|
118
|
+
output=root / "algorithm-plugin.json",
|
|
119
|
+
root=root,
|
|
120
|
+
algorithm=None,
|
|
121
|
+
host="0.0.0.0",
|
|
122
|
+
port=9000,
|
|
123
|
+
webui=True,
|
|
124
|
+
max_concurrency=1,
|
|
125
|
+
scratch_dir=None,
|
|
126
|
+
token=None,
|
|
127
|
+
registration=registration,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
self.assertEqual(
|
|
131
|
+
config.payload["registration"]["namespace"],
|
|
132
|
+
"demo-algorithm",
|
|
133
|
+
)
|
|
134
|
+
self.assertEqual(registration["namespace"], "ignored")
|
|
135
|
+
|
|
98
136
|
def test_repository_is_importable_during_generate_and_load(self) -> None:
|
|
99
137
|
with tempfile.TemporaryDirectory() as directory:
|
|
100
138
|
root = Path(directory)
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/context.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/errors.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/loader.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/models.py
RENAMED
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/release.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/runner.py
RENAMED
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/service.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_sdk_automation.py
RENAMED
|
File without changes
|
|
File without changes
|