funcd-bundle 0.12.0__tar.gz → 0.14.0__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.
@@ -1,32 +1,41 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: funcd-bundle
3
- Version: 0.12.0
3
+ Version: 0.14.0
4
4
  Summary: Bundle a Python funcd function and its locked dependencies for the funcd runtime
5
5
  Project-URL: Homepage, https://github.com/pyvvo/funcd-python
6
6
  Project-URL: Repository, https://github.com/pyvvo/funcd-python
7
7
  Author: The funcd Authors
8
8
  License-Expression: Apache-2.0
9
9
  Requires-Python: >=3.12
10
- Requires-Dist: pyyaml>=6.0.2
11
10
  Description-Content-Type: text/markdown
12
11
 
13
12
  # funcd-bundle
14
13
 
15
14
  Bundles a Python [funcd](https://github.com/pyvvo/funcd) function for deployment: the handler, the exact
16
15
  versions of its runtime dependencies from `uv.lock` installed for the funcd runtime's Linux platform, and
17
- its `funcdctl.yaml`. `funcdctl push` turns the bundle into one OCI artifact.
16
+ its contract file (funcd ADR-0247). `funcdctl push` turns the bundle into one OCI artifact.
18
17
 
19
18
  ```bash
20
19
  uv add --dev funcd-bundle
21
20
  uv run funcd-bundle # dist/<name>/ for linux/<host arch>
22
21
  uv run funcd-bundle --platform linux/amd64 # from any host
23
- funcdctl push dist/<name> <ref> --entry handler.py
22
+ funcdctl push dist/<name> <ref> # entry main.py, contract dist/<name>/<name>.contract.yaml
23
+ ```
24
+
25
+ - **Functions**: each entry of `[tool.funcd-bundle.functions]` is a function, its name mapped to its entry,
26
+ relative to the project root; without the table, the project is one function named after its directory,
27
+ from `src/main.py`. `funcdctl dev` reads the same entries. An entry in a subdirectory is copied with its
28
+ directory, so `src/<name>/main.py` lands as `main.py`, the entry `funcdctl push` assumes.
29
+ - **Contract**: each function's `<name>.contract.yaml` (or `.json`) at the project root is copied to its
30
+ bundle's root, where `funcdctl push` reads its runtime and contract. A function with neither, or both,
31
+ fails. A `funcdctl.yaml` or `<name>.funcdctl.yaml` fails the build: nothing reads it any more.
32
+
33
+ ```toml
34
+ [tool.funcd-bundle.functions]
35
+ orders = "src/orders/main.py"
36
+ orders-migrate = "src/orders-migrate/main.py"
24
37
  ```
25
38
 
26
- - **Functions**: every `<name>.funcdctl.yaml` in the project is a function `<name>`; with none,
27
- `funcdctl.yaml` is one named after the project directory. The handler is the manifest's `main`, else
28
- `<name>.py` (or `handler.py` for `funcdctl.yaml`) beside the manifest, as `funcdctl dev` reads it. A handler
29
- in a subdirectory is copied with its directory.
30
39
  - **Dependencies**: `uv export --frozen --no-dev` of the project, without `funcd-shim` and the packages
31
40
  only it needs (the runtime ships them). Registry packages install as wheels only (`--only-binary
32
41
  :all:`) for CPython 3.14 and manylinux glibc 2.36, with their locked hashes. Workspace members are built
@@ -2,19 +2,29 @@
2
2
 
3
3
  Bundles a Python [funcd](https://github.com/pyvvo/funcd) function for deployment: the handler, the exact
4
4
  versions of its runtime dependencies from `uv.lock` installed for the funcd runtime's Linux platform, and
5
- its `funcdctl.yaml`. `funcdctl push` turns the bundle into one OCI artifact.
5
+ its contract file (funcd ADR-0247). `funcdctl push` turns the bundle into one OCI artifact.
6
6
 
7
7
  ```bash
8
8
  uv add --dev funcd-bundle
9
9
  uv run funcd-bundle # dist/<name>/ for linux/<host arch>
10
10
  uv run funcd-bundle --platform linux/amd64 # from any host
11
- funcdctl push dist/<name> <ref> --entry handler.py
11
+ funcdctl push dist/<name> <ref> # entry main.py, contract dist/<name>/<name>.contract.yaml
12
+ ```
13
+
14
+ - **Functions**: each entry of `[tool.funcd-bundle.functions]` is a function, its name mapped to its entry,
15
+ relative to the project root; without the table, the project is one function named after its directory,
16
+ from `src/main.py`. `funcdctl dev` reads the same entries. An entry in a subdirectory is copied with its
17
+ directory, so `src/<name>/main.py` lands as `main.py`, the entry `funcdctl push` assumes.
18
+ - **Contract**: each function's `<name>.contract.yaml` (or `.json`) at the project root is copied to its
19
+ bundle's root, where `funcdctl push` reads its runtime and contract. A function with neither, or both,
20
+ fails. A `funcdctl.yaml` or `<name>.funcdctl.yaml` fails the build: nothing reads it any more.
21
+
22
+ ```toml
23
+ [tool.funcd-bundle.functions]
24
+ orders = "src/orders/main.py"
25
+ orders-migrate = "src/orders-migrate/main.py"
12
26
  ```
13
27
 
14
- - **Functions**: every `<name>.funcdctl.yaml` in the project is a function `<name>`; with none,
15
- `funcdctl.yaml` is one named after the project directory. The handler is the manifest's `main`, else
16
- `<name>.py` (or `handler.py` for `funcdctl.yaml`) beside the manifest, as `funcdctl dev` reads it. A handler
17
- in a subdirectory is copied with its directory.
18
28
  - **Dependencies**: `uv export --frozen --no-dev` of the project, without `funcd-shim` and the packages
19
29
  only it needs (the runtime ships them). Registry packages install as wheels only (`--only-binary
20
30
  :all:`) for CPython 3.14 and manylinux glibc 2.36, with their locked hashes. Workspace members are built
@@ -1,15 +1,13 @@
1
1
  [project]
2
2
  name = "funcd-bundle"
3
3
  # Not bumped per release: the vX.Y.Z git tag is the version, stamped into the build at publish time.
4
- version = "0.12.0"
4
+ version = "0.14.0"
5
5
  description = "Bundle a Python funcd function and its locked dependencies for the funcd runtime"
6
6
  authors = [{ name = "The funcd Authors" }]
7
7
  readme = "README.md"
8
8
  license = "Apache-2.0"
9
9
  requires-python = ">=3.12"
10
- dependencies = [
11
- "pyyaml>=6.0.2",
12
- ]
10
+ dependencies = []
13
11
 
14
12
  [project.urls]
15
13
  Homepage = "https://github.com/pyvvo/funcd-python"
@@ -30,7 +28,6 @@ dev = [
30
28
  "mypy>=1.13",
31
29
  "pytest>=8.3",
32
30
  "ruff>=0.8",
33
- "types-pyyaml>=6.0.12",
34
31
  ]
35
32
 
36
33
  [tool.mypy]
@@ -1,9 +1,9 @@
1
- """Build a funcd Python bundle (funcd ADR-0144).
1
+ """Build a funcd Python bundle (funcd ADR-0144, ADR-0247).
2
2
 
3
- A bundle is a directory `funcdctl push <dir> <ref> --entry <handler>` turns into one OCI layer (ADR-0089):
4
- the handler, the exact locked versions of its runtime dependencies installed for the runtime's Linux
5
- platform, and the function's ``funcdctl.yaml``. The runtime ships ``funcd_shim`` and its dependencies, so
6
- they are pruned.
3
+ A bundle is a directory `funcdctl push <dir> <ref>` turns into one OCI layer (ADR-0089): the handler, the
4
+ exact locked versions of its runtime dependencies installed for the runtime's Linux platform, and the
5
+ function's contract file ``<name>.contract.yaml`` (or ``.json``) at its root. The runtime ships ``funcd_shim``
6
+ and its dependencies, so they are pruned.
7
7
 
8
8
  Every container step copies its inputs in and its outputs out as a tar stream (``docker cp``) and binds no
9
9
  host path, so it also works from a job container that drives another Docker daemon.
@@ -25,8 +25,6 @@ import tomllib
25
25
  from dataclasses import dataclass
26
26
  from pathlib import Path
27
27
 
28
- import yaml
29
-
30
28
  BASE_IMAGE = "python:3.14-slim-bookworm"
31
29
  # BASE_IMAGE pinned per platform (its index is sha256:48b13b00…0f83), so a bundle builds from one image on
32
30
  # every machine. Not the index digest: the classic image store maps one digest to one image, so it cannot hold
@@ -37,9 +35,13 @@ BASE_IMAGES = {
37
35
  }
38
36
  PYTHON_VERSION = "3.14"
39
37
  GLIBC = "2_36"
38
+ CONTRACT_SUFFIXES = (".contract.yaml", ".contract.json")
40
39
  MANIFEST_SUFFIX = ".funcdctl.yaml"
41
40
  GENERIC_MANIFEST = "funcdctl.yaml"
42
- DEFAULT_HANDLER = "handler.py"
41
+ # a project without [tool.funcd-bundle.functions] is one function, named after its directory, from this entry
42
+ DEFAULT_ENTRY = "src/main.py"
43
+ # funcdctl push's --entry default for a python bundle (funcd ADR-0247 Decision 2)
44
+ PUSH_ENTRY = "main.py"
43
45
  # The runtime provides the shim and everything only it needs (ADR-0071: fastjsonschema).
44
46
  RUNTIME_PACKAGE = "funcd-shim"
45
47
  PLATFORMS = {"linux/amd64": "x86_64", "linux/arm64": "aarch64"}
@@ -57,7 +59,7 @@ class BundleError(Exception):
57
59
  @dataclass(frozen=True)
58
60
  class Function:
59
61
  name: str
60
- manifest: Path
62
+ contract: Path
61
63
  handler: Path
62
64
 
63
65
 
@@ -78,38 +80,66 @@ def host_platform() -> str:
78
80
 
79
81
 
80
82
  def discover(project: Path) -> dict[str, Function]:
81
- """Each <name>.funcdctl.yaml is a function <name>; with none, funcdctl.yaml is one named after the
82
- project. The handler is the manifest's `main`, relative to the project root, else funcdctl's default
83
- beside the manifest: <name>.py for a stem manifest, handler.py for the generic one (sdk Manifest.Main)."""
84
- manifests = {p.name[: -len(MANIFEST_SUFFIX)]: p for p in sorted(project.glob("*" + MANIFEST_SUFFIX))}
85
- defaults = {name: f"{name}.py" for name in manifests}
86
- if not manifests:
87
- generic = project / GENERIC_MANIFEST
88
- if not generic.is_file():
89
- raise BundleError(f"no {GENERIC_MANIFEST} or *{MANIFEST_SUFFIX} in {project}")
90
- manifests = {project.resolve().name: generic}
91
- defaults = {project.resolve().name: DEFAULT_HANDLER}
83
+ """The functions [tool.funcd-bundle.functions] lists (name → entry, relative to the project root), else
84
+ one named after the project directory, from src/main.py. Each needs its contract file <name>.contract.yaml
85
+ or .json at the project root; a funcdctl.yaml is refused, since nothing reads it (funcd ADR-0247)."""
86
+ old = sorted(project.glob("*" + MANIFEST_SUFFIX)) + [
87
+ p for p in [project / GENERIC_MANIFEST] if p.is_file()
88
+ ]
89
+ if old:
90
+ names = ", ".join(str(p) for p in old)
91
+ raise BundleError(
92
+ f"{names}: funcdctl.yaml is no longer read (funcd ADR-0247): put each function's runtime and "
93
+ f"contract in <name>.contract.yaml at {project}, its bindings in its Function resource file, and "
94
+ "its entry in [tool.funcd-bundle.functions]"
95
+ )
96
+ entries = _functions_table(project)
97
+ if entries is None:
98
+ entries = {project.resolve().name: DEFAULT_ENTRY}
92
99
  functions = {}
93
- for name, manifest in manifests.items():
94
- try:
95
- data = yaml.safe_load(manifest.read_text()) or {}
96
- except yaml.YAMLError as err:
97
- raise BundleError(f"{manifest}: {err}") from err
98
- if not isinstance(data, dict):
99
- raise BundleError(f"{manifest} is not a mapping")
100
- main = data.get("main") or defaults[name]
101
- handler = project / str(main)
100
+ for name, entry in entries.items():
101
+ handler = project / entry
102
102
  if not handler.is_file():
103
- raise BundleError(f"function {name}: handler {handler} (from {manifest.name}) does not exist")
104
- functions[name] = Function(name=name, manifest=manifest, handler=handler)
103
+ raise BundleError(f"function {name}: entry {handler} does not exist")
104
+ functions[name] = Function(name=name, contract=_contract(project, name), handler=handler)
105
105
  return functions
106
106
 
107
107
 
108
+ def _functions_table(project: Path) -> dict[str, str] | None:
109
+ pyproject = project / "pyproject.toml"
110
+ if not pyproject.is_file():
111
+ return None
112
+ try:
113
+ table = tomllib.loads(pyproject.read_text()).get("tool", {}).get("funcd-bundle", {}).get("functions")
114
+ except tomllib.TOMLDecodeError as err:
115
+ raise BundleError(f"{pyproject}: {err}") from err
116
+ if table is None:
117
+ return None
118
+ if not isinstance(table, dict) or not table or not all(isinstance(v, str) for v in table.values()):
119
+ raise BundleError(
120
+ f"{pyproject}: [tool.funcd-bundle.functions] must map each function name to its entry, a path"
121
+ )
122
+ return table
123
+
124
+
125
+ def _contract(project: Path, name: str) -> Path:
126
+ paths = [project / (name + suffix) for suffix in CONTRACT_SUFFIXES]
127
+ found = [p for p in paths if p.is_file()]
128
+ if not found:
129
+ raise BundleError(f"function {name} has no contract file: neither {paths[0]} nor {paths[1]} exists")
130
+ if len(found) > 1:
131
+ raise BundleError(f"function {name} has two contract files, {paths[0]} and {paths[1]}: keep one")
132
+ return found[0]
133
+
134
+
108
135
  def settings(project: Path) -> Settings:
109
136
  pyproject = project / "pyproject.toml"
110
137
  table: dict[str, object] = {}
111
138
  if pyproject.is_file():
112
- tool = tomllib.loads(pyproject.read_text()).get("tool", {})
139
+ try:
140
+ tool = tomllib.loads(pyproject.read_text()).get("tool", {})
141
+ except tomllib.TOMLDecodeError as err:
142
+ raise BundleError(f"{pyproject}: {err}") from err
113
143
  table = tool.get("funcd-bundle", {})
114
144
  hermetic = table.get("hermetic", False)
115
145
  post_install = table.get("post-install", [])
@@ -328,8 +358,8 @@ def _copy_out(cid: str, src: str, dest: Path) -> None:
328
358
 
329
359
 
330
360
  def _copy_handler(project: Path, fn: Function, out: Path) -> list[Path]:
331
- """The handler file alone when it sits at the project root, else its directory tree; then the manifest.
332
- Returns the copied Python sources."""
361
+ """The handler file alone when it sits at the project root, else its directory tree; then the contract
362
+ file, at the bundle's root, where funcdctl push reads it. Returns the copied Python sources."""
333
363
  source = fn.handler.parent
334
364
  if source.resolve() == project.resolve():
335
365
  shutil.copy2(fn.handler, out / fn.handler.name)
@@ -341,40 +371,10 @@ def _copy_handler(project: Path, fn: Function, out: Path) -> list[Path]:
341
371
 
342
372
  shutil.copytree(source, out, dirs_exist_ok=True, ignore=ignore)
343
373
  sources = [p for p in source.rglob("*.py") if not SKIPPED.intersection(p.relative_to(source).parts)]
344
- # inside the bundle the handler sits at the root, so `main` must name it there (funcdctl dev reads it); a
345
- # manifest without one gets one, else the bundle's generic manifest would default to handler.py
346
- (out / GENERIC_MANIFEST).write_text(_with_main(fn.manifest.read_text(), fn.handler.name))
374
+ shutil.copy2(fn.contract, out / fn.contract.name)
347
375
  return sources
348
376
 
349
377
 
350
- def _with_main(text: str, handler: str) -> str:
351
- """The manifest with its top-level `main` set to handler. The YAML parser finds the key, however it is
352
- written; the rest stays as written, since a PyYAML dump would re-resolve YAML 1.1 scalars funcd reads as
353
- YAML 1.2 text."""
354
- entry = f"main: {handler}"
355
- root = yaml.compose(text, Loader=yaml.SafeLoader)
356
- if isinstance(root, yaml.MappingNode):
357
- for key, value in root.value:
358
- if isinstance(key, yaml.ScalarNode) and key.value == "main":
359
- start, end = key.start_mark.index, value.end_mark.index
360
- # a block scalar's span ends past its line break
361
- if text[start:end].endswith("\n"):
362
- entry += "\n"
363
- return text[:start] + entry + text[end:]
364
- if root.flow_style:
365
- # a block line after a flow mapping is a second root: the key goes inside, after its brace
366
- brace = next(
367
- t
368
- for t in yaml.scan(text, Loader=yaml.SafeLoader)
369
- if isinstance(t, yaml.FlowMappingStartToken)
370
- )
371
- at = brace.end_mark.index
372
- return text[:at] + entry + (", " if root.value else "") + text[at:]
373
- if text and not text.endswith("\n"):
374
- text += "\n"
375
- return text + entry + "\n"
376
-
377
-
378
378
  def _top_level_modules(out: Path) -> set[str]:
379
379
  """The importable top-level names of the vendored distributions, from each RECORD."""
380
380
  names: set[str] = set()
@@ -1,4 +1,4 @@
1
- """`uv run funcd-bundle`: build every function of the project in the current directory (funcd ADR-0144)."""
1
+ """`uv run funcd-bundle`: build every function of the project in the current directory (funcd ADR-0247)."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -6,14 +6,23 @@ import argparse
6
6
  import sys
7
7
  from pathlib import Path
8
8
 
9
- from funcd_bundle.bundle import BASE_IMAGE, PLATFORMS, BundleError, bundle, discover, host_platform, settings
9
+ from funcd_bundle.bundle import (
10
+ BASE_IMAGE,
11
+ PLATFORMS,
12
+ PUSH_ENTRY,
13
+ BundleError,
14
+ bundle,
15
+ discover,
16
+ host_platform,
17
+ settings,
18
+ )
10
19
 
11
20
 
12
21
  def main(argv: list[str] | None = None) -> int:
13
22
  parser = argparse.ArgumentParser(
14
23
  prog="funcd-bundle",
15
24
  description="Bundle each function of this uv project for the funcd runtime: the handler, its locked "
16
- "dependencies for the target Linux platform, and its funcdctl.yaml.",
25
+ "dependencies for the target Linux platform, and its contract file.",
17
26
  )
18
27
  parser.add_argument("names", nargs="*", metavar="NAME", help="functions to build (default: all)")
19
28
  parser.add_argument("--out", default="dist", type=Path, help="output directory (default: dist)")
@@ -47,7 +56,8 @@ def main(argv: list[str] | None = None) -> int:
47
56
  out = args.out / platform.replace("/", "-") / name
48
57
  path = bundle(project, name, platform, out, hermetic=hermetic, check=args.check)
49
58
  print(f"built {name} for {platform}: {path}")
50
- print(f"funcdctl push {path} <ref> --entry {functions[name].handler.name}")
59
+ entry = functions[name].handler.name
60
+ print(f"funcdctl push {path} <ref>" + ("" if entry == PUSH_ENTRY else f" --entry {entry}"))
51
61
  except BundleError as err:
52
62
  print(f"funcd-bundle: {err}", file=sys.stderr)
53
63
  return 1
@@ -1,4 +1,4 @@
1
- """Scenario tests for funcd-bundle (funcd ADR-0144).
1
+ """Scenario tests for funcd-bundle (funcd ADR-0144, ADR-0247).
2
2
 
3
3
  They build a small uv workspace and lock it, so they need the network; the container cases also need Docker.
4
4
  Each skips with the reason when its requirement is absent. CI has both.
@@ -16,7 +16,6 @@ from collections.abc import Iterator
16
16
  from pathlib import Path
17
17
 
18
18
  import pytest
19
- import yaml
20
19
 
21
20
  from funcd_bundle import BundleError, bundle, discover, host_platform
22
21
  from funcd_bundle.bundle import BASE_IMAGES, PLATFORMS, _check
@@ -24,7 +23,8 @@ from funcd_bundle.cli import main
24
23
 
25
24
  FOREIGN = {"linux/arm64": "linux/amd64", "linux/amd64": "linux/arm64"}
26
25
  ELF_MACHINE = {"linux/amd64": 0x3E, "linux/arm64": 0xB7}
27
- MANIFEST = "runtime: python314\nhandler: handle\nmain: src/handler.py\n"
26
+ CONTRACT = "runtime: python314\ninput:\n type: object\noutput:\n type: object\n"
27
+ CONTRACT_JSON = '{"runtime": "python314", "input": {"type": "object"}, "output": {"type": "object"}}\n'
28
28
  EXAMPLES = Path(__file__).resolve().parents[2] / "examples"
29
29
 
30
30
 
@@ -84,8 +84,8 @@ def workspace(tmp_path_factory: pytest.TempPathFactory) -> Iterator[Path]:
84
84
  '[dependency-groups]\ndev = ["iniconfig>=2"]\n\n'
85
85
  "[tool.uv.sources]\npy-common = { workspace = true }\n"
86
86
  ),
87
- "functions/reader/funcdctl.yaml": MANIFEST,
88
- "functions/reader/src/handler.py": (
87
+ "functions/reader/reader.contract.yaml": CONTRACT,
88
+ "functions/reader/src/main.py": (
89
89
  "import orjson\nimport py_common\n\n\n"
90
90
  "def handle(ctx, event):\n return orjson.loads(orjson.dumps({'c': py_common.hello()}))\n"
91
91
  ),
@@ -106,8 +106,11 @@ def _locked(project: Path, name: str) -> str:
106
106
  def test_scenario_bundle_vendors_locked_deps(workspace: Path, tmp_path: Path) -> None:
107
107
  out = bundle(workspace, "reader", host_platform(), tmp_path / "reader", hermetic=False, check=False)
108
108
 
109
- assert (out / "handler.py").is_file()
110
- assert (out / "funcdctl.yaml").read_text() == MANIFEST.replace("main: src/handler.py", "main: handler.py")
109
+ assert (out / "main.py").is_file()
110
+ assert (out / "reader.contract.yaml").read_text() == CONTRACT, (
111
+ "the contract file sits at the bundle's root"
112
+ )
113
+ assert not (out / "funcdctl.yaml").exists(), "no manifest is written"
111
114
  assert not (out / "tests").exists(), "the handler's tests are not bundled"
112
115
  assert (out / f"orjson-{_locked(workspace, 'orjson')}.dist-info").is_dir()
113
116
  assert (out / f"six-{_locked(workspace, 'six')}.dist-info").is_dir()
@@ -151,8 +154,8 @@ def test_scenario_bundle_refuses_source_builds(tmp_path: Path) -> None:
151
154
  '[project]\nname = "old"\nversion = "0.1.0"\nrequires-python = ">=3.12"\n'
152
155
  'dependencies = ["pyyaml==5.4.1"]\n'
153
156
  ),
154
- "funcdctl.yaml": MANIFEST,
155
- "src/handler.py": "import yaml\n",
157
+ "old.contract.yaml": CONTRACT,
158
+ "src/main.py": "import yaml\n",
156
159
  },
157
160
  )
158
161
  _uv("lock", "--python", "3.14", cwd=project)
@@ -217,13 +220,14 @@ def test_post_install_needs_hermetic(tmp_path: Path) -> None:
217
220
  {
218
221
  "pyproject.toml": (
219
222
  '[project]\nname = "x"\nversion = "0"\n\n[tool.funcd-bundle]\npost-install = ["true"]\n'
223
+ '\n[tool.funcd-bundle.functions]\nx = "src/main.py"\n'
220
224
  ),
221
- "funcdctl.yaml": MANIFEST,
222
- "src/handler.py": "",
225
+ "x.contract.yaml": CONTRACT,
226
+ "src/main.py": "",
223
227
  },
224
228
  )
225
229
  with pytest.raises(BundleError, match="hermetic"):
226
- bundle(tmp_path, tmp_path.name, host_platform(), tmp_path / "out", hermetic=False, check=False)
230
+ bundle(tmp_path, "x", host_platform(), tmp_path / "out", hermetic=False, check=False)
227
231
 
228
232
 
229
233
  def test_cli_post_install_needs_hermetic(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
@@ -232,9 +236,10 @@ def test_cli_post_install_needs_hermetic(tmp_path: Path, monkeypatch: pytest.Mon
232
236
  {
233
237
  "pyproject.toml": (
234
238
  '[project]\nname = "x"\nversion = "0"\n\n[tool.funcd-bundle]\npost-install = ["true"]\n'
239
+ '\n[tool.funcd-bundle.functions]\nx = "src/main.py"\n'
235
240
  ),
236
- "funcdctl.yaml": MANIFEST,
237
- "src/handler.py": "",
241
+ "x.contract.yaml": CONTRACT,
242
+ "src/main.py": "",
238
243
  },
239
244
  )
240
245
  monkeypatch.chdir(tmp_path)
@@ -244,123 +249,121 @@ def test_cli_post_install_needs_hermetic(tmp_path: Path, monkeypatch: pytest.Mon
244
249
  assert not (tmp_path / "out").exists(), "nothing is bundled"
245
250
 
246
251
 
247
- def test_default_handler_follows_funcdctl(tmp_path: Path) -> None:
252
+ # scenario: dev-python-in-place (the entries funcd-bundle and funcdctl dev share)
253
+ def test_functions_table_names_each_entry(tmp_path: Path) -> None:
248
254
  _write(
249
- tmp_path, {"counter.funcdctl.yaml": "runtime: python314\n", "counter.py": "def handle(c, e): ...\n"}
250
- )
251
- assert discover(tmp_path)["counter"].handler == tmp_path / "counter.py", (
252
- "a stem manifest's default is <name>.py"
253
- )
254
- _write(tmp_path / "g", {"funcdctl.yaml": "runtime: python314\n", "handler.py": ""})
255
- assert discover(tmp_path / "g")["g"].handler == tmp_path / "g" / "handler.py", (
256
- "the generic default is handler.py"
255
+ tmp_path,
256
+ {
257
+ "pyproject.toml": (
258
+ '[project]\nname = "app"\nversion = "0"\n\n[tool.funcd-bundle.functions]\n'
259
+ 'intake = "src/intake/main.py"\nstats = "src/stats/main.py"\n'
260
+ ),
261
+ "intake.contract.yaml": CONTRACT,
262
+ "stats.contract.json": CONTRACT_JSON,
263
+ "src/intake/main.py": "",
264
+ "src/stats/main.py": "",
265
+ },
257
266
  )
267
+ found = discover(tmp_path)
268
+ assert {n: f.handler for n, f in found.items()} == {
269
+ "intake": tmp_path / "src/intake/main.py",
270
+ "stats": tmp_path / "src/stats/main.py",
271
+ }
272
+ assert found["stats"].contract == tmp_path / "stats.contract.json"
273
+
274
+
275
+ def test_without_the_table_one_function_named_after_the_folder(tmp_path: Path) -> None:
276
+ project = tmp_path / "counter"
277
+ _write(project, {"counter.contract.yaml": CONTRACT, "src/main.py": ""})
278
+ [(name, fn)] = discover(project).items()
279
+ assert (name, fn.handler) == ("counter", project / "src/main.py")
258
280
 
259
281
 
260
- def test_issue_r54_every_example_manifest_names_a_handler_it_ships() -> None:
261
- examples = sorted({m.parent for m in EXAMPLES.glob("*/*funcdctl.yaml")})
262
- assert examples, f"no example manifests under {EXAMPLES}"
282
+ def test_every_example_names_an_entry_and_a_contract_it_ships() -> None:
283
+ examples = sorted(p.parent for p in EXAMPLES.glob("*/pyproject.toml"))
284
+ assert examples, f"no example projects under {EXAMPLES}"
263
285
  missing = []
264
286
  for example in examples:
265
287
  try:
266
288
  discover(example)
267
289
  except BundleError as err:
268
290
  missing.append(str(err))
269
- assert not missing, f"funcdctl dev and funcd-bundle cannot find these handlers: {missing}"
291
+ assert not missing, f"funcdctl dev and funcd-bundle cannot find these functions: {missing}"
270
292
 
271
293
 
272
- def test_issue_154_stem_manifest_without_main_names_bundled_handler(tmp_path: Path) -> None:
273
- manifest = "runtime: python314\nhandler: handle\n"
274
- project = tmp_path / "project"
275
- _write(
276
- project,
277
- {
278
- "pyproject.toml": '[project]\nname = "extract"\nversion = "0.1.0"\nrequires-python = ">=3.12"\n',
279
- "extract.funcdctl.yaml": manifest,
280
- "extract.py": "def handle(ctx, event): ...\n",
281
- },
282
- )
283
- _uv("lock", "--offline", "--python", "3.14", cwd=project)
284
- out = bundle(project, "extract", host_platform(), tmp_path / "extract", hermetic=False, check=False)
285
-
286
- assert discover(out)["extract"].handler == out / "extract.py", (
287
- "the bundle's generic manifest resolves to the bundled handler, not handler.py"
288
- )
289
- assert (out / "funcdctl.yaml").read_text() == manifest + "main: extract.py\n"
290
-
291
-
292
- @pytest.mark.parametrize(
293
- "main",
294
- [
295
- '"main": src/handler.py\n',
296
- "'main': src/handler.py\n",
297
- "main:\n src/handler.py\n",
298
- ],
299
- ids=["double-quoted-key", "single-quoted-key", "value-on-next-line"],
300
- )
301
- def test_issue_r29_main_key_is_rewritten_not_duplicated(tmp_path: Path, main: str) -> None:
294
+ # scenario: bundlers-copy-the-contract
295
+ def test_scenario_bundlers_copy_the_contract(tmp_path: Path) -> None:
302
296
  project = tmp_path / "reader"
303
297
  _write(
304
298
  project,
305
299
  {
306
300
  "pyproject.toml": '[project]\nname = "reader"\nversion = "0.1.0"\nrequires-python = ">=3.12"\n',
307
- "funcdctl.yaml": "runtime: python314\n" + main + "handler: handle\n",
308
- "src/handler.py": "def handle(ctx, event): ...\n",
301
+ "reader.contract.json": CONTRACT_JSON,
302
+ "src/main.py": "def handle(ctx, event): ...\n",
309
303
  },
310
304
  )
311
305
  _uv("lock", "--offline", "--python", "3.14", cwd=project)
312
306
  out = bundle(project, "reader", host_platform(), tmp_path / "out", hermetic=False, check=False)
313
307
 
314
- assert (out / "funcdctl.yaml").read_text() == "runtime: python314\nmain: handler.py\nhandler: handle\n", (
315
- "the manifest's one main names the bundled handler"
308
+ assert (out / "reader.contract.json").read_text() == CONTRACT_JSON
309
+ assert (out / "main.py").is_file(), "the entry sits at the bundle's root, where push looks for main.py"
310
+ assert not (out / "funcdctl.yaml").exists()
311
+
312
+
313
+ def test_scenario_bundlers_copy_the_contract_a_function_without_one_fails(tmp_path: Path) -> None:
314
+ _write(tmp_path / "reader", {"src/main.py": ""})
315
+ with pytest.raises(BundleError, match=r"no contract file: neither .*reader\.contract\.yaml nor .*\.json"):
316
+ discover(tmp_path / "reader")
317
+
318
+
319
+ def test_two_contract_files_of_one_function_fail(tmp_path: Path) -> None:
320
+ _write(
321
+ tmp_path / "reader",
322
+ {"reader.contract.yaml": CONTRACT, "reader.contract.json": CONTRACT_JSON, "src/main.py": ""},
316
323
  )
324
+ with pytest.raises(BundleError, match="two contract files"):
325
+ discover(tmp_path / "reader")
317
326
 
318
327
 
319
- @pytest.mark.parametrize(
320
- ("manifest", "want"),
321
- [
322
- ("{runtime: python314, handler: handle}\n", {"runtime": "python314", "handler": "handle"}),
323
- ("!!map &m {\n runtime: python314,\n}\n", {"runtime": "python314"}),
324
- ("{}\n", {}),
325
- ],
326
- ids=["one-line", "tagged-multi-line", "empty"],
327
- )
328
- def test_issue_r49_flow_mapping_manifest_gets_main(
329
- tmp_path: Path, manifest: str, want: dict[str, str]
328
+ # scenario: old-manifest-refused
329
+ @pytest.mark.parametrize("manifest", ["funcdctl.yaml", "reader.funcdctl.yaml"])
330
+ def test_scenario_old_manifest_refused(
331
+ tmp_path: Path, manifest: str, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
330
332
  ) -> None:
331
333
  project = tmp_path / "reader"
332
- _write(
333
- project,
334
- {
335
- "pyproject.toml": '[project]\nname = "reader"\nversion = "0.1.0"\nrequires-python = ">=3.12"\n',
336
- "funcdctl.yaml": manifest,
337
- "handler.py": "def handle(ctx, event): ...\n",
338
- },
339
- )
340
- _uv("lock", "--offline", "--python", "3.14", cwd=project)
341
- out = bundle(project, "reader", host_platform(), tmp_path / "out", hermetic=False, check=False)
334
+ _write(project, {"reader.contract.yaml": CONTRACT, "src/main.py": "", manifest: "runtime: python314\n"})
335
+ monkeypatch.chdir(project)
342
336
 
343
- assert yaml.safe_load((out / "funcdctl.yaml").read_text()) == want | {"main": "handler.py"}, (
344
- "the bundle's manifest is valid YAML whose main names the bundled handler"
337
+ assert main(["--no-check", "--out", str(tmp_path / "out")]) == 1
338
+ err = capsys.readouterr().err
339
+ assert f"{project / manifest}: funcdctl.yaml is no longer read" in err, err
340
+ assert "<name>.contract.yaml" in err and "[tool.funcd-bundle.functions]" in err, (
341
+ "it names what replaces it"
345
342
  )
343
+ assert not (tmp_path / "out").exists(), "nothing is bundled"
346
344
 
347
345
 
348
- def test_missing_handler_is_named(tmp_path: Path) -> None:
349
- _write(tmp_path, {"funcdctl.yaml": "runtime: python314\nmain: src/nope.py\n"})
346
+ def test_missing_entry_is_named(tmp_path: Path) -> None:
347
+ _write(
348
+ tmp_path,
349
+ {
350
+ "pyproject.toml": '[project]\nname = "x"\n\n[tool.funcd-bundle.functions]\nx = "src/nope.py"\n',
351
+ "x.contract.yaml": CONTRACT,
352
+ },
353
+ )
350
354
  with pytest.raises(BundleError, match="nope.py"):
351
- bundle(tmp_path, tmp_path.name, host_platform(), tmp_path / "out", hermetic=False, check=False)
355
+ discover(tmp_path)
352
356
 
353
357
 
354
- def test_issue_r48_invalid_manifest_yaml_is_reported(
358
+ def test_invalid_functions_table_is_reported(
355
359
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
356
360
  ) -> None:
357
- _write(tmp_path, {"funcdctl.yaml": "runtime: [\n", "handler.py": ""})
361
+ _write(tmp_path, {"pyproject.toml": '[project]\nname = "x"\n\n[tool.funcd-bundle.functions]\nx = 1\n'})
358
362
  monkeypatch.chdir(tmp_path)
359
363
 
360
- assert main(["--no-check", "--out", str(tmp_path / "out")]) == 1, "a manifest that does not parse fails"
364
+ assert main(["--no-check", "--out", str(tmp_path / "out")]) == 1
361
365
  err = capsys.readouterr().err
362
- assert err.startswith(f"funcd-bundle: {tmp_path / 'funcdctl.yaml'}: "), err
363
- assert "while parsing a flow node" in err, "the parser message names the problem"
366
+ assert "[tool.funcd-bundle.functions] must map each function name to its entry" in err, err
364
367
  assert "Traceback" not in err
365
368
 
366
369
 
@@ -83,27 +83,21 @@ wheels = [
83
83
  name = "funcd-bundle"
84
84
  version = "0.1.0"
85
85
  source = { editable = "." }
86
- dependencies = [
87
- { name = "pyyaml" },
88
- ]
89
86
 
90
87
  [package.dev-dependencies]
91
88
  dev = [
92
89
  { name = "mypy" },
93
90
  { name = "pytest" },
94
91
  { name = "ruff" },
95
- { name = "types-pyyaml" },
96
92
  ]
97
93
 
98
94
  [package.metadata]
99
- requires-dist = [{ name = "pyyaml", specifier = ">=6.0.2" }]
100
95
 
101
96
  [package.metadata.requires-dev]
102
97
  dev = [
103
98
  { name = "mypy", specifier = ">=1.13" },
104
99
  { name = "pytest", specifier = ">=8.3" },
105
100
  { name = "ruff", specifier = ">=0.8" },
106
- { name = "types-pyyaml", specifier = ">=6.0.12" },
107
101
  ]
108
102
 
109
103
  [[package]]
@@ -331,52 +325,6 @@ wheels = [
331
325
  { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
332
326
  ]
333
327
 
334
- [[package]]
335
- name = "pyyaml"
336
- version = "6.0.3"
337
- source = { registry = "https://pypi.org/simple" }
338
- sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
339
- wheels = [
340
- { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
341
- { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
342
- { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
343
- { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
344
- { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
345
- { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
346
- { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
347
- { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
348
- { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
349
- { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
350
- { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
351
- { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
352
- { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
353
- { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
354
- { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
355
- { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
356
- { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
357
- { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
358
- { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
359
- { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
360
- { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
361
- { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
362
- { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
363
- { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
364
- { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
365
- { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
366
- { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
367
- { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
368
- { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
369
- { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
370
- { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
371
- { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
372
- { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
373
- { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
374
- { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
375
- { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
376
- { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
377
- { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
378
- ]
379
-
380
328
  [[package]]
381
329
  name = "ruff"
382
330
  version = "0.16.10"
@@ -402,15 +350,6 @@ wheels = [
402
350
  { url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" },
403
351
  ]
404
352
 
405
- [[package]]
406
- name = "types-pyyaml"
407
- version = "6.0.12.20260906"
408
- source = { registry = "https://pypi.org/simple" }
409
- sdist = { url = "https://files.pythonhosted.org/packages/90/6e/abec85b9013db5b934b0280a6dd104904d84f7bcbaab2e2f3def87ac7463/types_pyyaml-6.0.12.20260906.tar.gz", hash = "sha256:f59c1cc05010b833d2d72287bbaa72610106b28d42d89a907313117faba85212", size = 18649, upload-time = "2026-09-06T06:35:35.362Z" }
410
- wheels = [
411
- { url = "https://files.pythonhosted.org/packages/15/c0/fc0644b7ddcfb969e95845837143cb5173ddd6e06ee4ba5fc493cd9329b7/types_pyyaml-6.0.12.20260906-py3-none-any.whl", hash = "sha256:bca893ff0d51df5c9053137d5d0e6ccd36e939a196356f1d5c16372422f5137b", size = 21282, upload-time = "2026-09-06T06:35:34.372Z" },
412
- ]
413
-
414
353
  [[package]]
415
354
  name = "typing-extensions"
416
355
  version = "4.16.0"
File without changes