ls-algorithm-plugin-sdk 0.3.0__py3-none-any.whl → 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  ```
@@ -2,7 +2,7 @@ algorithm_plugin_sdk/__init__.py,sha256=OKj2VcNXTwZmRgPlECI6QUBlxW1YLtflr4RAgs6w
2
2
  algorithm_plugin_sdk/algorithm.py,sha256=INjX-ygMQfHk8D1AZbEEdCw1P4VNQLGF989lP1J1H1k,1025
3
3
  algorithm_plugin_sdk/cli.py,sha256=V9HFJ98iyhM1q6PZqd-366g1AoKuieC_Fn4u9RJcMNI,1252
4
4
  algorithm_plugin_sdk/context.py,sha256=Kj-eDZg7PgX6xNzrWrQs37UfepTP8fXv4_8hCgsAIDA,11016
5
- algorithm_plugin_sdk/deployment.py,sha256=hFX6kJXia2gF2mOCDJlgWK-BAUT6tAvr2t70u32mg1A,13823
5
+ algorithm_plugin_sdk/deployment.py,sha256=yv6UCMtbX1pglfq7H7N2xI4_-bM-YLSPXFST5m_pW-g,13931
6
6
  algorithm_plugin_sdk/errors.py,sha256=DNmI2c36Pd7K6tTaonqfEgpBzwX0o6pXzYrc8r2Z--k,730
7
7
  algorithm_plugin_sdk/loader.py,sha256=XKmyqbJMoUMdKoo2eRQFGdbsmlz6NBgtUeIdqzumJFs,2561
8
8
  algorithm_plugin_sdk/models.py,sha256=FIwUm8QDK8L37pV7AcCMnmYlDP-2l93zp8yc0KSu4yo,12196
@@ -12,7 +12,7 @@ algorithm_plugin_sdk/runner.py,sha256=583Y9_7efvW5rrmM2nt316NEpCRO6kd8dKYjFRjgtC
12
12
  algorithm_plugin_sdk/service.py,sha256=Xj99UiCZtqtu7U4IVghAr4Mu3I9nR1AvP6vNzdD4sAk,24002
13
13
  algorithm_plugin_sdk/webui_app.py,sha256=Gvt2FPt0xZBjYi39sqTQqcosllifYeJam49_mGfkEVw,1530
14
14
  algorithm_plugin_sdk/cli_impl/__init__.py,sha256=uLknQVGipNd7eSJegDHZS0NVowMa0qAaOf63wFI3zHM,78
15
- algorithm_plugin_sdk/cli_impl/configure.py,sha256=acCGDJ6Khn3G8hay8ISsYVlKDmhDH1i-UpNZU5bb0fU,11668
15
+ algorithm_plugin_sdk/cli_impl/configure.py,sha256=fr_xDLh82YKV8Csuxi5M2z4dPpXWv9j7SZUHa1n14dA,12496
16
16
  algorithm_plugin_sdk/cli_impl/parsing.py,sha256=TDX45Spu4WMrczuzTMN9ZC2SYyHE11OPBCvN45yPsxs,1399
17
17
  algorithm_plugin_sdk/cli_impl/run.py,sha256=EnqqCdGWYK-iB5s1DMlGRXNpnCWA25xoAex0f4l36Ic,5893
18
18
  algorithm_plugin_sdk/cli_impl/serve.py,sha256=9_nJcdzerwqczGm7xTIcMYKShpmoltYD6r_nBQjAlWU,4324
@@ -23,8 +23,8 @@ algorithm_plugin_sdk/webui/__init__.py,sha256=cvtaktJXz_DYG4QbV5ppoqY2bFaMLuiUcT
23
23
  algorithm_plugin_sdk/webui/app.css,sha256=5HGDKlbUFLZMgCknlRuwCjiNcCr8poIF5YGHchyaamE,2965
24
24
  algorithm_plugin_sdk/webui/app.js,sha256=0YsXsocEY3yDseG3jh0RhzI8xo_bYmsbSxdN3FdCswQ,7657
25
25
  algorithm_plugin_sdk/webui/index.html,sha256=Z222OuaacWdbr0s-sjJq6wmg45u96tIneM5-LP55Z8I,2261
26
- ls_algorithm_plugin_sdk-0.3.0.dist-info/METADATA,sha256=YewSjuTdWCstRq4c6h4-fy-7pSbs31JRoui_J5GhV7M,2943
27
- ls_algorithm_plugin_sdk-0.3.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
28
- ls_algorithm_plugin_sdk-0.3.0.dist-info/entry_points.txt,sha256=TU0R_TxuB5OuOiZ5BHOEmUPtHvN3v8ARUW8d3PztHcE,67
29
- ls_algorithm_plugin_sdk-0.3.0.dist-info/top_level.txt,sha256=8lsgxZ8HJGLlzJ5Rt8CiVzw9Ccme286i3akbmeMjBos,21
30
- ls_algorithm_plugin_sdk-0.3.0.dist-info/RECORD,,
26
+ ls_algorithm_plugin_sdk-0.3.1.dist-info/METADATA,sha256=m3-0HP49-i6sFKAgZjcsWLMGdUdVL-jLUag4siJ9i3Q,2317
27
+ ls_algorithm_plugin_sdk-0.3.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
28
+ ls_algorithm_plugin_sdk-0.3.1.dist-info/entry_points.txt,sha256=TU0R_TxuB5OuOiZ5BHOEmUPtHvN3v8ARUW8d3PztHcE,67
29
+ ls_algorithm_plugin_sdk-0.3.1.dist-info/top_level.txt,sha256=8lsgxZ8HJGLlzJ5Rt8CiVzw9Ccme286i3akbmeMjBos,21
30
+ ls_algorithm_plugin_sdk-0.3.1.dist-info/RECORD,,