flowmesh-sdk-stack 0.1.1__tar.gz → 0.1.3__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 (19) hide show
  1. {flowmesh_sdk_stack-0.1.1/src/flowmesh_sdk_stack.egg-info → flowmesh_sdk_stack-0.1.3}/PKG-INFO +2 -2
  2. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/pyproject.toml +2 -2
  3. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3/src/flowmesh_sdk_stack.egg-info}/PKG-INFO +2 -2
  4. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_sdk_stack.egg-info/requires.txt +1 -1
  5. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/env_schema.py +35 -8
  6. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/workers.py +93 -41
  7. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/LICENSE +0 -0
  8. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/README.md +0 -0
  9. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/setup.cfg +0 -0
  10. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_sdk_stack.egg-info/SOURCES.txt +0 -0
  11. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_sdk_stack.egg-info/dependency_links.txt +0 -0
  12. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_sdk_stack.egg-info/top_level.txt +0 -0
  13. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/__init__.py +0 -0
  14. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/docker.py +0 -0
  15. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/doctor.py +0 -0
  16. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/env.py +0 -0
  17. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/images.py +0 -0
  18. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/node_client.py +0 -0
  19. {flowmesh_sdk_stack-0.1.1 → flowmesh_sdk_stack-0.1.3}/src/flowmesh_stack/paths.py +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowmesh-sdk-stack
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: FlowMesh SDK stack client
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.12
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: flowmesh-sdk==0.1.1
9
+ Requires-Dist: flowmesh-sdk==0.1.3
10
10
  Requires-Dist: httpx>=0.27.0
11
11
  Requires-Dist: pyyaml>=6.0.0
12
12
  Requires-Dist: docker>=7.1.0
@@ -4,14 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flowmesh-sdk-stack"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "FlowMesh SDK stack client"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
11
11
  license = "Apache-2.0"
12
12
  license-files = ["LICENSE"]
13
13
  dependencies = [
14
- "flowmesh-sdk==0.1.1",
14
+ "flowmesh-sdk==0.1.3",
15
15
  "httpx>=0.27.0",
16
16
  "pyyaml>=6.0.0",
17
17
  "docker>=7.1.0",
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowmesh-sdk-stack
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: FlowMesh SDK stack client
5
5
  License-Expression: Apache-2.0
6
6
  Requires-Python: >=3.12
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
- Requires-Dist: flowmesh-sdk==0.1.1
9
+ Requires-Dist: flowmesh-sdk==0.1.3
10
10
  Requires-Dist: httpx>=0.27.0
11
11
  Requires-Dist: pyyaml>=6.0.0
12
12
  Requires-Dist: docker>=7.1.0
@@ -1,4 +1,4 @@
1
- flowmesh-sdk==0.1.1
1
+ flowmesh-sdk==0.1.3
2
2
  httpx>=0.27.0
3
3
  pyyaml>=6.0.0
4
4
  docker>=7.1.0
@@ -1,6 +1,7 @@
1
1
  """Environment schema definitions and pure validation helpers."""
2
2
 
3
3
  import enum
4
+ import operator
4
5
  from collections.abc import Callable, Iterable, Mapping
5
6
  from dataclasses import dataclass, field
6
7
  from logging import _nameToLevel as LOG_LEVELS
@@ -34,7 +35,9 @@ class EnvVar:
34
35
  use_default: bool = False
35
36
  choices: Iterable[str] | None = None
36
37
  min_value: float | None = None
38
+ min_inclusive: bool = True
37
39
  max_value: float | None = None
40
+ max_inclusive: bool = True
38
41
  min_length: int | None = None
39
42
  ensure_path: Literal["error", "warn", "create"] | None = None
40
43
  url_schemes: set[str] | None = None
@@ -133,19 +136,13 @@ def validate_env_values(
133
136
  if int_value is None:
134
137
  errors.append(f"{var.key} must be an integer")
135
138
  continue
136
- if var.min_value is not None and int_value < var.min_value:
137
- errors.append(f"{var.key} must be >= {int(var.min_value)}")
138
- if var.max_value is not None and int_value > var.max_value:
139
- errors.append(f"{var.key} must be <= {int(var.max_value)}")
139
+ _check_value_range(var, int_value, errors)
140
140
  case EnvVarType.FLOAT:
141
141
  float_value = parse_float(raw)
142
142
  if float_value is None:
143
143
  errors.append(f"{var.key} must be a number")
144
144
  continue
145
- if var.min_value is not None and float_value < var.min_value:
146
- errors.append(f"{var.key} must be >= {var.min_value}")
147
- if var.max_value is not None and float_value > var.max_value:
148
- errors.append(f"{var.key} must be <= {var.max_value}")
145
+ _check_value_range(var, float_value, errors)
149
146
  case EnvVarType.BOOL:
150
147
  if parse_bool(raw) is None:
151
148
  errors.append(
@@ -213,6 +210,36 @@ def require_all_or_none(
213
210
  errors.append(f"Either all or none of {', '.join(keys)} must be set")
214
211
 
215
212
 
213
+ _COMPARE_OPS = {
214
+ "<": operator.lt,
215
+ "<=": operator.le,
216
+ ">": operator.gt,
217
+ ">=": operator.ge,
218
+ }
219
+
220
+
221
+ def _check_value_range(var: EnvVar, value: float, errors: list[str]) -> None:
222
+ comp: Literal["<", "<=", ">", ">="]
223
+ if var.min_value is not None:
224
+ comp = ">=" if var.min_inclusive else ">"
225
+ _check_bound(var, value, var.min_value, comp, errors)
226
+ if var.max_value is not None:
227
+ comp = "<=" if var.max_inclusive else "<"
228
+ _check_bound(var, value, var.max_value, comp, errors)
229
+
230
+
231
+ def _check_bound(
232
+ var: EnvVar,
233
+ value: float,
234
+ bound: float,
235
+ comparison: Literal["<", "<=", ">", ">="],
236
+ errors: list[str],
237
+ ) -> None:
238
+ op = _COMPARE_OPS[comparison]
239
+ if not op(value, bound):
240
+ errors.append(f"{var.key} must be {comparison} {bound}")
241
+
242
+
216
243
  def _ensure_path(raw: str, var: EnvVar, errors: list[str], warnings: list[str]) -> None:
217
244
  if not raw:
218
245
  errors.append(f"{var.key} must be a non-empty path")
@@ -65,12 +65,20 @@ def create_workers(
65
65
  targets: str = "all",
66
66
  config_paths: list[Path] | None = None,
67
67
  config_raw: list[str] | None = None,
68
+ name_template: str | None = None,
69
+ slug: str | None = None,
68
70
  ) -> list[tuple[str, dict[str, Any]]]:
69
71
  """Create node workers from configs or built-in cpu/gpu presets.
70
72
 
71
73
  When ``kind`` is "gpu", if ``count`` is equal to 1, a single worker with the
72
74
  specified GPU targets will be created. If ``count`` is greater than 1, one worker
73
75
  will be created per GPU target, and the number of targets must match ``count``.
76
+
77
+ For the built-in cpu/gpu presets, ``slug`` prefixes the generated worker
78
+ aliases so names are scoped to a stack, and ``name_template`` overrides the
79
+ naming entirely (placeholders ``{slug}``, ``{kind}``, ``{idx}``, ``{gpu}``).
80
+ Both are ignored when ``config_paths`` / ``config_raw`` are provided, since
81
+ those payloads carry their own aliases.
74
82
  """
75
83
  payloads = _payloads_for_worker_create(
76
84
  kind=kind,
@@ -78,6 +86,8 @@ def create_workers(
78
86
  targets=targets,
79
87
  config_paths=config_paths,
80
88
  config_raw=config_raw,
89
+ name_template=name_template,
90
+ slug=slug,
81
91
  )
82
92
  if not payloads:
83
93
  return []
@@ -162,12 +172,47 @@ def detect_gpu_targets(targets: str) -> list[str]:
162
172
  return [item.strip() for item in result.stdout.splitlines() if item.strip()]
163
173
 
164
174
 
175
+ _ALIAS_FIELDS = "{slug}, {kind}, {idx}, {gpu}"
176
+
177
+
178
+ class _StrictFormatDict(dict[str, Any]):
179
+ def __missing__(self, key: str) -> Any:
180
+ raise KeyError(key)
181
+
182
+
183
+ def _worker_alias(
184
+ kind: str,
185
+ idx: int,
186
+ slug: str | None,
187
+ template: str | None,
188
+ gpu: str | None = None,
189
+ ) -> str:
190
+ if template is None:
191
+ template = (
192
+ ("{slug}_" if slug else "")
193
+ + f"worker_{kind}_"
194
+ + ("{idx}" if gpu is None else "{gpu}")
195
+ )
196
+ fields: dict[str, Any] = {"slug": slug or "", "kind": kind, "idx": idx}
197
+ if gpu is not None:
198
+ fields["gpu"] = gpu
199
+ try:
200
+ return template.format_map(_StrictFormatDict(fields))
201
+ except (KeyError, IndexError, ValueError) as exc:
202
+ raise FlowMeshError(
203
+ f"invalid --name-template ({exc}); "
204
+ f"available placeholders: {_ALIAS_FIELDS}"
205
+ ) from exc
206
+
207
+
165
208
  def _payloads_for_worker_create(
166
209
  kind: str,
167
210
  count: int,
168
211
  targets: str,
169
212
  config_paths: list[Path] | None,
170
213
  config_raw: list[str] | None,
214
+ name_template: str | None = None,
215
+ slug: str | None = None,
171
216
  ) -> list[tuple[str, str]]:
172
217
  if count < 1:
173
218
  raise FlowMeshError("Worker count must be at least 1.")
@@ -183,25 +228,17 @@ def _payloads_for_worker_create(
183
228
  _extend_payloads(payloads, f"worker from raw#{idx}", raw)
184
229
  return payloads
185
230
 
231
+ specs: list[tuple[str, dict[str, Any], str]] # alias, worker_config, label
186
232
  if kind == "cpu":
187
- return [
233
+ specs = [
188
234
  (
189
- json.dumps(
190
- {
191
- "provider": "docker",
192
- "init_on_start": True,
193
- "worker_config": {
194
- "worker_type": "cpu",
195
- "worker_alias": f"worker_cpu_{idx}",
196
- },
197
- }
198
- ),
235
+ (alias := _worker_alias("cpu", idx, slug, name_template)),
236
+ {"worker_type": "cpu", "worker_alias": alias},
199
237
  "CPU worker",
200
238
  )
201
239
  for idx in range(count)
202
240
  ]
203
-
204
- if kind == "gpu":
241
+ elif kind == "gpu":
205
242
  raw_gpu_ids = detect_gpu_targets(targets)
206
243
  gpu_ids: list[int] = []
207
244
  for raw_gpu_id in raw_gpu_ids:
@@ -217,47 +254,62 @@ def _payloads_for_worker_create(
217
254
  f"Consider setting count={len(gpu_ids)} or specifying exactly "
218
255
  f"{count} GPU targets."
219
256
  )
220
- gpu_payloads = [
257
+ specs = [
221
258
  (
222
- json.dumps(
223
- {
224
- "provider": "docker",
225
- "init_on_start": True,
226
- "worker_config": {
227
- "worker_type": "gpu",
228
- "cuda_devices": [gpu_id],
229
- "worker_alias": f"worker_gpu_{gpu_id}",
230
- },
231
- }
259
+ (
260
+ alias := _worker_alias(
261
+ "gpu", idx, slug, name_template, gpu=str(gpu_id)
262
+ )
232
263
  ),
264
+ {
265
+ "worker_type": "gpu",
266
+ "cuda_devices": [gpu_id],
267
+ "worker_alias": alias,
268
+ },
233
269
  f"GPU worker for GPU {gpu_id}",
234
270
  )
235
- for gpu_id in gpu_ids
271
+ for idx, gpu_id in enumerate(gpu_ids)
236
272
  ]
237
273
  else:
238
274
  worker_suffix = "all" if targets == "all" else "_".join(raw_gpu_ids)
239
- gpu_payloads = [
275
+ specs = [
240
276
  (
241
- json.dumps(
242
- {
243
- "provider": "docker",
244
- "init_on_start": True,
245
- "worker_config": {
246
- "worker_type": "gpu",
247
- "cuda_devices": gpu_ids,
248
- "worker_alias": f"worker_gpu_{worker_suffix}",
249
- },
250
- }
277
+ (
278
+ alias := _worker_alias(
279
+ "gpu", 0, slug, name_template, gpu=worker_suffix
280
+ )
251
281
  ),
282
+ {
283
+ "worker_type": "gpu",
284
+ "cuda_devices": gpu_ids,
285
+ "worker_alias": alias,
286
+ },
252
287
  f"GPU worker for GPUs {', '.join(raw_gpu_ids)}",
253
288
  )
254
289
  ]
290
+ else:
291
+ raise FlowMeshError("worker up expects kind cpu|gpu or use --config")
292
+
293
+ aliases = [alias for alias, _, _ in specs]
294
+ if len(set(aliases)) != len(aliases):
295
+ raise FlowMeshError(
296
+ "--name-template produced duplicate worker names; "
297
+ "include {idx} or {gpu} to disambiguate"
298
+ )
255
299
 
256
- if not gpu_payloads:
257
- raise FlowMeshError("No GPUs detected or specified.")
258
- return gpu_payloads
259
-
260
- raise FlowMeshError("worker up expects kind cpu|gpu or use --config")
300
+ return [
301
+ (
302
+ json.dumps(
303
+ {
304
+ "provider": "docker",
305
+ "init_on_start": True,
306
+ "worker_config": worker_config,
307
+ }
308
+ ),
309
+ label,
310
+ )
311
+ for _, worker_config, label in specs
312
+ ]
261
313
 
262
314
 
263
315
  def _extend_payloads(