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.
Files changed (44) hide show
  1. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/PKG-INFO +6 -17
  2. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/README.md +5 -16
  3. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/pyproject.toml +1 -1
  4. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/configure.py +55 -42
  5. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/deployment.py +2 -0
  6. {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
  7. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_configure.py +70 -36
  8. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_deployment.py +38 -0
  9. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/setup.cfg +0 -0
  10. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/__init__.py +0 -0
  11. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/algorithm.py +0 -0
  12. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli.py +0 -0
  13. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/__init__.py +0 -0
  14. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/parsing.py +0 -0
  15. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/run.py +0 -0
  16. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/cli_impl/serve.py +0 -0
  17. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/context.py +0 -0
  18. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/errors.py +0 -0
  19. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/examples/__init__.py +0 -0
  20. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/examples/example_algorithm.py +0 -0
  21. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/examples/simulated_algorithm.py +0 -0
  22. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/loader.py +0 -0
  23. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/models.py +0 -0
  24. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/registration.py +0 -0
  25. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/release.py +0 -0
  26. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/runner.py +0 -0
  27. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/service.py +0 -0
  28. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui/__init__.py +0 -0
  29. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui/app.css +0 -0
  30. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui/app.js +0 -0
  31. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui/index.html +0 -0
  32. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/src/algorithm_plugin_sdk/webui_app.py +0 -0
  33. {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
  34. {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
  35. {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
  36. {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
  37. {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
  38. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_cli.py +0 -0
  39. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_models.py +0 -0
  40. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_registration.py +0 -0
  41. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_release.py +0 -0
  42. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_runner.py +0 -0
  43. {ls_algorithm_plugin_sdk-0.3.0 → ls_algorithm_plugin_sdk-0.3.1}/tests/test_sdk_automation.py +0 -0
  44. {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.0
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。同一个 `execute()`
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 工作树根目录,并且至少有一个 commit。
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
- --public-url http://10.0.0.4:9000 \
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 '.[service]'
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。同一个 `execute()`
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 工作树根目录,并且至少有一个 commit。
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
- --public-url http://10.0.0.4:9000 \
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 '.[service]'
72
+ python -m pip install .
84
73
  python -m unittest discover -s tests -v
85
74
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ls-algorithm-plugin-sdk"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "Protocol-independent runtime SDK for dataset algorithms"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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 = requested or repository.name
107
+ name = repository.name
125
108
  if name.endswith(".service"):
126
109
  raise ValueError(
127
- "service name must not include the .service suffix"
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
- "service name may contain only letters, numbers, "
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 _quote_systemd_path(value: Path) -> str:
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
- escaped = (
126
+ return (
144
127
  text.replace("\\", "\\\\")
145
128
  .replace('"', '\\"')
146
129
  .replace("%", "%%")
147
130
  )
148
- return f'"{escaped}"'
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={_quote_systemd_path(root)}",
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
- "publicUrl": args.public_url,
331
- "inputRoot": args.input_root,
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
- return 0
433
+ print(f" sudo systemctl status {service_name}.service --no-pager")
434
+ return 0
@@ -154,6 +154,8 @@ class DeploymentConfig:
154
154
  instance,
155
155
  repository=repository,
156
156
  )
157
+ if registration is not None:
158
+ registration = {**registration, "namespace": release.name}
157
159
  release_path = release.save(
158
160
  repository / "release-manifest.json"
159
161
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ls-algorithm-plugin-sdk
3
- Version: 0.3.0
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。同一个 `execute()`
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 工作树根目录,并且至少有一个 commit。
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
- --public-url http://10.0.0.4:9000 \
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 '.[service]'
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.*publicUrl",
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
- "--namespace",
232
- "demo",
233
- "--node-name",
234
- "node-1",
234
+ "--port",
235
+ "9030",
235
236
  ]
236
237
  )
237
238
 
238
- first = registration_from_args(args, repository)
239
- second = registration_from_args(args, repository)
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, None),
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
- for name in ("bad name", "../demo", "demo.service"):
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(repository, 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
- 'WorkingDirectory="/srv/demo algorithm"\n'
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)