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.
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/PKG-INFO +17 -8
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/README.md +16 -6
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/pyproject.toml +2 -5
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/src/funcd_bundle/bundle.py +65 -65
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/src/funcd_bundle/cli.py +14 -4
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/tests/test_bundle.py +95 -92
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/uv.lock +0 -61
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/.gitignore +0 -0
- {funcd_bundle-0.12.0 → funcd_bundle-0.14.0}/src/funcd_bundle/__init__.py +0 -0
|
@@ -1,32 +1,41 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: funcd-bundle
|
|
3
|
-
Version: 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
|
|
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>
|
|
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
|
|
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>
|
|
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.
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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,
|
|
94
|
-
|
|
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}:
|
|
104
|
-
functions[name] = Function(name=name,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
88
|
-
"functions/reader/src/
|
|
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 / "
|
|
110
|
-
assert (out / "
|
|
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
|
-
"
|
|
155
|
-
"src/
|
|
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
|
-
"
|
|
222
|
-
"src/
|
|
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,
|
|
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
|
-
"
|
|
237
|
-
"src/
|
|
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
|
-
|
|
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,
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
|
261
|
-
examples = sorted(
|
|
262
|
-
assert examples, f"no example
|
|
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
|
|
291
|
+
assert not missing, f"funcdctl dev and funcd-bundle cannot find these functions: {missing}"
|
|
270
292
|
|
|
271
293
|
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
"
|
|
308
|
-
"src/
|
|
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 / "
|
|
315
|
-
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
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
|
|
344
|
-
|
|
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
|
|
349
|
-
_write(
|
|
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
|
-
|
|
355
|
+
discover(tmp_path)
|
|
352
356
|
|
|
353
357
|
|
|
354
|
-
def
|
|
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, {"
|
|
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
|
|
364
|
+
assert main(["--no-check", "--out", str(tmp_path / "out")]) == 1
|
|
361
365
|
err = capsys.readouterr().err
|
|
362
|
-
assert
|
|
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
|
|
File without changes
|