spadelib 0.1.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.
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: spadelib
3
+ Version: 0.1.0
4
+ Summary: Spade block authoring library for Python
5
+ Keywords: spade,blocks,pipeline,data
6
+ Author: krbundy
7
+ Author-email: krbundy <kenneth.bundy@maine.edu>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: pydantic>=2.12.5
16
+ Requires-Dist: pyyaml>=6.0
17
+ Requires-Python: >=3.12
18
+ Project-URL: Repository, https://github.com/krbundy/psae_monorepo
19
+ Project-URL: Issues, https://github.com/krbundy/psae_monorepo/issues
20
+ Description-Content-Type: text/markdown
21
+
22
+ # Spade Python Library
23
+
24
+ The Python library for authoring [Spade](../../spec/core.md) blocks. Define a handler function, call `run()`, and the library handles parameter loading, input discovery, and output writing.
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ uv add spadelib
30
+ ```
31
+
32
+
33
+ ```bash
34
+ uv sync
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ```python
40
+ from spadelib import run, RasterFile
41
+
42
+ def handler(source: RasterFile, buffer_distance: int) -> RasterFile:
43
+ # Your processing logic here
44
+ result_path = process(source.path, buffer_distance)
45
+ return RasterFile(path=result_path)
46
+
47
+ if __name__ == "__main__":
48
+ run(handler)
49
+ ```
50
+
51
+ That's it. The `run()` function:
52
+
53
+ 1. Loads scalar parameters from `params.yaml`
54
+ 2. Scans `inputs/` for file-based arguments, matched by name to your function parameters
55
+ 3. Calls your handler with the combined arguments
56
+ 4. Writes the return value to `outputs/`
57
+
58
+ ## Types
59
+
60
+ Use type hints on your handler to control how inputs are loaded and outputs are written.
61
+
62
+ ### File Types
63
+
64
+ | Type | Description |
65
+ |------|-------------|
66
+ | `File` | Generic single file |
67
+ | `RasterFile` | Raster data (e.g., GeoTIFF) |
68
+ | `VectorFile` | Vector data (e.g., GeoJSON) |
69
+ | `TabularFile` | Tabular data (e.g., CSV) |
70
+ | `JsonFile` | JSON data |
71
+
72
+ Each has a `path: str` field pointing to the file location.
73
+
74
+ ### Directory Type
75
+
76
+ | Type | Description |
77
+ |------|-------------|
78
+ | `Directory` | Directory-based input (e.g., shapefiles) |
79
+
80
+ Has a `path: str` field pointing to the directory.
81
+
82
+ ### Collection Types
83
+
84
+ | Type | Description |
85
+ |------|-------------|
86
+ | `FileCollection` | Collection of files |
87
+ | `RasterFileCollection` | Collection of raster files |
88
+ | `VectorFileCollection` | Collection of vector files |
89
+ | `TabularFileCollection` | Collection of tabular files |
90
+
91
+ Each has a `paths: list[str]` field.
92
+
93
+ ## How Inputs Are Resolved
94
+
95
+ The handler's parameter names are matched against two sources:
96
+
97
+ - **Scalar parameters** (`str`, `int`, `float`, `bool`): loaded from `params.yaml`
98
+ - **File-based inputs**: discovered from subdirectories in `inputs/`, where each subdirectory name matches a parameter name
99
+
100
+ ```
101
+ params.yaml # scalar args: {"resolution": 10, "method": "nearest"}
102
+ inputs/
103
+ reference/
104
+ data.tif # -> handler(reference=RasterFile(path="inputs/reference/data.tif"))
105
+ target/
106
+ data.tif # -> handler(target=RasterFile(path="inputs/target/data.tif"))
107
+ ```
108
+
109
+ Type hints determine how each input is constructed:
110
+
111
+ - `File` subclass: expects a single file in the subdirectory
112
+ - `Directory`: uses the subdirectory path itself
113
+ - `FileCollection` subclass: collects all files in the subdirectory
114
+
115
+ ## Output Handling
116
+
117
+ Return a typed value from your handler and `run()` writes it to `outputs/`:
118
+
119
+ ```python
120
+ # Single output
121
+ def handler(source: RasterFile) -> RasterFile:
122
+ return RasterFile(path="result.tif")
123
+
124
+ # Multiple outputs
125
+ def handler(source: RasterFile) -> dict:
126
+ return {
127
+ "raster": RasterFile(path="result.tif"),
128
+ "summary": JsonFile(path="stats.json"),
129
+ }
130
+ ```
131
+
132
+ Output names are resolved from `block.yaml` (if available) or inferred from the return type.
133
+
134
+ ## The `build()` Function
135
+
136
+ Generate a block manifest from a handler's signature:
137
+
138
+ ```python
139
+ from spadelib import build, RasterFile, VectorFile
140
+
141
+ def handler(raster: RasterFile, buffer: int) -> VectorFile:
142
+ """Converts raster boundaries to vector polygons."""
143
+ ...
144
+
145
+ manifest = build(handler)
146
+ # {'description': 'Converts raster boundaries to vector polygons.',
147
+ # 'inputs': {'raster': {'type': 'file', 'format': 'GeoTIFF'},
148
+ # 'buffer': {'type': 'number'}},
149
+ # 'outputs': {'vector': {'type': 'file', 'format': 'GeoJSON'}}}
150
+ ```
151
+
152
+ ## Testing
153
+
154
+ ```bash
155
+ uv run pytest
156
+ ```
@@ -0,0 +1,135 @@
1
+ # Spade Python Library
2
+
3
+ The Python library for authoring [Spade](../../spec/core.md) blocks. Define a handler function, call `run()`, and the library handles parameter loading, input discovery, and output writing.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ uv add spadelib
9
+ ```
10
+
11
+
12
+ ```bash
13
+ uv sync
14
+ ```
15
+
16
+ ## Quick Start
17
+
18
+ ```python
19
+ from spadelib import run, RasterFile
20
+
21
+ def handler(source: RasterFile, buffer_distance: int) -> RasterFile:
22
+ # Your processing logic here
23
+ result_path = process(source.path, buffer_distance)
24
+ return RasterFile(path=result_path)
25
+
26
+ if __name__ == "__main__":
27
+ run(handler)
28
+ ```
29
+
30
+ That's it. The `run()` function:
31
+
32
+ 1. Loads scalar parameters from `params.yaml`
33
+ 2. Scans `inputs/` for file-based arguments, matched by name to your function parameters
34
+ 3. Calls your handler with the combined arguments
35
+ 4. Writes the return value to `outputs/`
36
+
37
+ ## Types
38
+
39
+ Use type hints on your handler to control how inputs are loaded and outputs are written.
40
+
41
+ ### File Types
42
+
43
+ | Type | Description |
44
+ |------|-------------|
45
+ | `File` | Generic single file |
46
+ | `RasterFile` | Raster data (e.g., GeoTIFF) |
47
+ | `VectorFile` | Vector data (e.g., GeoJSON) |
48
+ | `TabularFile` | Tabular data (e.g., CSV) |
49
+ | `JsonFile` | JSON data |
50
+
51
+ Each has a `path: str` field pointing to the file location.
52
+
53
+ ### Directory Type
54
+
55
+ | Type | Description |
56
+ |------|-------------|
57
+ | `Directory` | Directory-based input (e.g., shapefiles) |
58
+
59
+ Has a `path: str` field pointing to the directory.
60
+
61
+ ### Collection Types
62
+
63
+ | Type | Description |
64
+ |------|-------------|
65
+ | `FileCollection` | Collection of files |
66
+ | `RasterFileCollection` | Collection of raster files |
67
+ | `VectorFileCollection` | Collection of vector files |
68
+ | `TabularFileCollection` | Collection of tabular files |
69
+
70
+ Each has a `paths: list[str]` field.
71
+
72
+ ## How Inputs Are Resolved
73
+
74
+ The handler's parameter names are matched against two sources:
75
+
76
+ - **Scalar parameters** (`str`, `int`, `float`, `bool`): loaded from `params.yaml`
77
+ - **File-based inputs**: discovered from subdirectories in `inputs/`, where each subdirectory name matches a parameter name
78
+
79
+ ```
80
+ params.yaml # scalar args: {"resolution": 10, "method": "nearest"}
81
+ inputs/
82
+ reference/
83
+ data.tif # -> handler(reference=RasterFile(path="inputs/reference/data.tif"))
84
+ target/
85
+ data.tif # -> handler(target=RasterFile(path="inputs/target/data.tif"))
86
+ ```
87
+
88
+ Type hints determine how each input is constructed:
89
+
90
+ - `File` subclass: expects a single file in the subdirectory
91
+ - `Directory`: uses the subdirectory path itself
92
+ - `FileCollection` subclass: collects all files in the subdirectory
93
+
94
+ ## Output Handling
95
+
96
+ Return a typed value from your handler and `run()` writes it to `outputs/`:
97
+
98
+ ```python
99
+ # Single output
100
+ def handler(source: RasterFile) -> RasterFile:
101
+ return RasterFile(path="result.tif")
102
+
103
+ # Multiple outputs
104
+ def handler(source: RasterFile) -> dict:
105
+ return {
106
+ "raster": RasterFile(path="result.tif"),
107
+ "summary": JsonFile(path="stats.json"),
108
+ }
109
+ ```
110
+
111
+ Output names are resolved from `block.yaml` (if available) or inferred from the return type.
112
+
113
+ ## The `build()` Function
114
+
115
+ Generate a block manifest from a handler's signature:
116
+
117
+ ```python
118
+ from spadelib import build, RasterFile, VectorFile
119
+
120
+ def handler(raster: RasterFile, buffer: int) -> VectorFile:
121
+ """Converts raster boundaries to vector polygons."""
122
+ ...
123
+
124
+ manifest = build(handler)
125
+ # {'description': 'Converts raster boundaries to vector polygons.',
126
+ # 'inputs': {'raster': {'type': 'file', 'format': 'GeoTIFF'},
127
+ # 'buffer': {'type': 'number'}},
128
+ # 'outputs': {'vector': {'type': 'file', 'format': 'GeoJSON'}}}
129
+ ```
130
+
131
+ ## Testing
132
+
133
+ ```bash
134
+ uv run pytest
135
+ ```
@@ -0,0 +1,42 @@
1
+ [project]
2
+ # Distribution (PyPI) name. The import name stays "spadelib" via
3
+ # [tool.uv.build-backend].module-name below, so block code keeps using
4
+ # `import spadelib` regardless of this.
5
+ name = "spadelib"
6
+ version = "0.1.0"
7
+ description = "Spade block authoring library for Python"
8
+ readme = "README.md"
9
+ requires-python = ">=3.12"
10
+ license = "Apache-2.0"
11
+ authors = [
12
+ { name = "krbundy", email = "kenneth.bundy@maine.edu" }
13
+ ]
14
+ keywords = ["spade", "blocks", "pipeline", "data"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Operating System :: OS Independent",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = [
24
+ "pydantic>=2.12.5",
25
+ "pyyaml>=6.0",
26
+ ]
27
+
28
+ [project.urls]
29
+ Repository = "https://github.com/krbundy/psae_monorepo"
30
+ Issues = "https://github.com/krbundy/psae_monorepo/issues"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "pytest>=8.0",
35
+ ]
36
+
37
+ [build-system]
38
+ requires = ["uv_build>=0.10.0,<0.11.0"]
39
+ build-backend = "uv_build"
40
+
41
+ [tool.uv.build-backend]
42
+ module-name = "spadelib"
@@ -0,0 +1,31 @@
1
+ from spadelib.types import (
2
+ Directory,
3
+ File,
4
+ FileCollection,
5
+ JsonFile,
6
+ RasterFile,
7
+ RasterFileCollection,
8
+ TabularFile,
9
+ TabularFileCollection,
10
+ VectorFile,
11
+ VectorFileCollection,
12
+ )
13
+ from spadelib.run import run
14
+ from spadelib.build import build
15
+ from spadelib.secrets import get_secret
16
+
17
+ __all__ = [
18
+ "File",
19
+ "Directory",
20
+ "RasterFile",
21
+ "VectorFile",
22
+ "TabularFile",
23
+ "JsonFile",
24
+ "FileCollection",
25
+ "RasterFileCollection",
26
+ "VectorFileCollection",
27
+ "TabularFileCollection",
28
+ "run",
29
+ "build",
30
+ "get_secret",
31
+ ]
@@ -0,0 +1,101 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+ import yaml
7
+
8
+ from spadelib.types import (
9
+ Directory,
10
+ File,
11
+ FileCollection,
12
+ JsonFile,
13
+ RasterFile,
14
+ RasterFileCollection,
15
+ TabularFile,
16
+ TabularFileCollection,
17
+ VectorFile,
18
+ VectorFileCollection,
19
+ )
20
+
21
+ _TYPE_TO_DEFAULT_NAME: dict[type, str] = {
22
+ File: "file",
23
+ RasterFile: "raster",
24
+ VectorFile: "vector",
25
+ TabularFile: "tabular",
26
+ JsonFile: "json",
27
+ Directory: "directory",
28
+ FileCollection: "files",
29
+ RasterFileCollection: "rasters",
30
+ VectorFileCollection: "vectors",
31
+ TabularFileCollection: "tables",
32
+ }
33
+
34
+
35
+ def read_block_manifest() -> dict | None:
36
+ """Attempt to read the block manifest for output declarations.
37
+
38
+ Checks (in order):
39
+ 1. SPADE_BLOCK_MANIFEST environment variable
40
+ 2. block.yaml in the current working directory
41
+ """
42
+ manifest_path = os.environ.get("SPADE_BLOCK_MANIFEST")
43
+ if manifest_path and Path(manifest_path).exists():
44
+ with open(manifest_path, "r") as f:
45
+ manifest = yaml.safe_load(f)
46
+ return manifest.get("outputs") if manifest else None
47
+
48
+ block_yaml = Path("block.yaml")
49
+ if block_yaml.exists():
50
+ with open(block_yaml, "r") as f:
51
+ manifest = yaml.safe_load(f)
52
+ return manifest.get("outputs") if manifest else None
53
+
54
+ return None
55
+
56
+
57
+ def _infer_output_name(value: Any) -> str:
58
+ """Infer output name from the type of the return value."""
59
+ for type_cls, name in _TYPE_TO_DEFAULT_NAME.items():
60
+ if type(value) is type_cls:
61
+ return name
62
+ return type(value).__name__.lower()
63
+
64
+
65
+ def _write_single_output(name: str, value: Any) -> None:
66
+ """Write a single output value to outputs/<name>/."""
67
+ output_dir = Path("outputs") / name
68
+ output_dir.mkdir(parents=True, exist_ok=True)
69
+
70
+ if isinstance(value, FileCollection):
71
+ for file_path in value.paths:
72
+ src = Path(file_path)
73
+ shutil.copy2(src, output_dir / src.name)
74
+ elif isinstance(value, Directory):
75
+ src = Path(value.path)
76
+ for item in src.iterdir():
77
+ if item.is_file():
78
+ shutil.copy2(item, output_dir / item.name)
79
+ elif item.is_dir():
80
+ shutil.copytree(item, output_dir / item.name)
81
+ elif isinstance(value, File):
82
+ src = Path(value.path)
83
+ shutil.copy2(src, output_dir / src.name)
84
+
85
+
86
+ def write_outputs(result: Any, manifest_outputs: dict | None = None) -> None:
87
+ """Write handler return value(s) to the outputs/ directory."""
88
+ if result is None:
89
+ return
90
+
91
+ Path("outputs").mkdir(exist_ok=True)
92
+
93
+ if isinstance(result, dict):
94
+ for name, value in result.items():
95
+ _write_single_output(name, value)
96
+ elif isinstance(result, (File, Directory, FileCollection)):
97
+ if manifest_outputs and len(manifest_outputs) == 1:
98
+ name = next(iter(manifest_outputs))
99
+ else:
100
+ name = _infer_output_name(result)
101
+ _write_single_output(name, result)
@@ -0,0 +1,89 @@
1
+ from pathlib import Path
2
+ from typing import Any, Callable, get_type_hints
3
+
4
+ import yaml
5
+
6
+ from spadelib.types import (
7
+ Directory,
8
+ File,
9
+ FileCollection,
10
+ )
11
+
12
+
13
+ def load_params() -> dict:
14
+ """Load scalar parameters from params.yaml."""
15
+ params_path = Path("params.yaml")
16
+ if not params_path.exists():
17
+ return {}
18
+ with open(params_path, "r") as f:
19
+ params = yaml.safe_load(f)
20
+ return params if params is not None else {}
21
+
22
+
23
+ def scan_inputs(type_hints: dict[str, type]) -> dict[str, Any]:
24
+ """Scan the inputs/ directory and build typed arguments.
25
+
26
+ Args:
27
+ type_hints: Mapping of parameter name -> expected type from the
28
+ handler's annotations.
29
+
30
+ Returns:
31
+ Dict mapping parameter name -> typed value.
32
+ """
33
+ inputs_dir = Path("inputs")
34
+ if not inputs_dir.exists():
35
+ return {}
36
+
37
+ result = {}
38
+ for subdir in sorted(inputs_dir.iterdir()):
39
+ if not subdir.is_dir():
40
+ continue
41
+
42
+ name = subdir.name
43
+ expected_type = type_hints.get(name)
44
+
45
+ files = sorted([f for f in subdir.iterdir() if f.is_file()])
46
+
47
+ if expected_type is not None:
48
+ try:
49
+ if issubclass(expected_type, Directory):
50
+ result[name] = expected_type(path=str(subdir))
51
+ continue
52
+ elif issubclass(expected_type, FileCollection):
53
+ result[name] = expected_type(paths=[str(f) for f in files])
54
+ continue
55
+ elif issubclass(expected_type, File):
56
+ if not files:
57
+ raise ValueError(
58
+ f"Input '{name}' expects a file but directory "
59
+ f"'{subdir}' is empty"
60
+ )
61
+ result[name] = expected_type(path=str(files[0]))
62
+ continue
63
+ except TypeError:
64
+ pass # expected_type is not a class, fall through
65
+
66
+ # Default: infer from file count
67
+ if not files:
68
+ raise ValueError(f"Input directory '{subdir}' is empty")
69
+ if len(files) == 1:
70
+ result[name] = File(path=str(files[0]))
71
+ else:
72
+ result[name] = FileCollection(paths=[str(f) for f in files])
73
+
74
+ return result
75
+
76
+
77
+ def build_function_args(fn: Callable) -> dict[str, Any]:
78
+ """Build the full arguments dict for the handler function.
79
+
80
+ Merges scalar parameters from params.yaml with file-based inputs
81
+ from the inputs/ directory. Inputs take precedence over params.
82
+ """
83
+ type_hints = get_type_hints(fn)
84
+ type_hints.pop("return", None)
85
+
86
+ params = load_params()
87
+ inputs = scan_inputs(type_hints)
88
+
89
+ return params | inputs
@@ -0,0 +1,88 @@
1
+ import inspect
2
+ from typing import Any, Callable, get_type_hints
3
+
4
+ from spadelib.types import (
5
+ Directory,
6
+ File,
7
+ FileCollection,
8
+ JsonFile,
9
+ RasterFile,
10
+ RasterFileCollection,
11
+ TabularFile,
12
+ TabularFileCollection,
13
+ VectorFile,
14
+ VectorFileCollection,
15
+ )
16
+
17
+ _PYTHON_TYPE_TO_MANIFEST: dict[type, dict[str, str]] = {
18
+ File: {"type": "file"},
19
+ RasterFile: {"type": "file", "format": "GeoTIFF"},
20
+ VectorFile: {"type": "file", "format": "GeoJSON"},
21
+ TabularFile: {"type": "file", "format": "CSV"},
22
+ JsonFile: {"type": "json"},
23
+ Directory: {"type": "directory"},
24
+ FileCollection: {"type": "collection", "item_type": "file"},
25
+ RasterFileCollection: {"type": "collection", "item_type": "file", "format": "GeoTIFF"},
26
+ VectorFileCollection: {"type": "collection", "item_type": "file", "format": "GeoJSON"},
27
+ TabularFileCollection: {"type": "collection", "item_type": "file", "format": "CSV"},
28
+ str: {"type": "string"},
29
+ int: {"type": "number"},
30
+ float: {"type": "number"},
31
+ bool: {"type": "boolean"},
32
+ }
33
+
34
+ _TYPE_TO_OUTPUT_NAME: dict[type, str] = {
35
+ File: "file",
36
+ RasterFile: "raster",
37
+ VectorFile: "vector",
38
+ TabularFile: "tabular",
39
+ JsonFile: "json",
40
+ Directory: "directory",
41
+ FileCollection: "files",
42
+ RasterFileCollection: "rasters",
43
+ VectorFileCollection: "vectors",
44
+ TabularFileCollection: "tables",
45
+ }
46
+
47
+
48
+ def build(fn: Callable) -> dict:
49
+ """Generate a block manifest dict from a handler function's signature.
50
+
51
+ Inspects the function's type hints and docstring to produce a dict
52
+ that can be serialized to block.yaml.
53
+
54
+ Args:
55
+ fn: The handler function to inspect.
56
+
57
+ Returns:
58
+ A dict representing the block manifest (inputs, outputs, description).
59
+ """
60
+ hints = get_type_hints(fn)
61
+ sig = inspect.signature(fn)
62
+
63
+ inputs: dict[str, Any] = {}
64
+ for param_name in sig.parameters:
65
+ param_type = hints.get(param_name)
66
+ if param_type is None:
67
+ continue
68
+ manifest_entry = _PYTHON_TYPE_TO_MANIFEST.get(param_type)
69
+ if manifest_entry is not None:
70
+ inputs[param_name] = dict(manifest_entry)
71
+
72
+ outputs: dict[str, Any] = {}
73
+ return_type = hints.get("return")
74
+ if return_type is not None and return_type is not type(None):
75
+ manifest_entry = _PYTHON_TYPE_TO_MANIFEST.get(return_type)
76
+ if manifest_entry is not None:
77
+ output_name = _TYPE_TO_OUTPUT_NAME.get(return_type, "output")
78
+ outputs[output_name] = dict(manifest_entry)
79
+
80
+ manifest: dict[str, Any] = {}
81
+
82
+ if fn.__doc__:
83
+ manifest["description"] = fn.__doc__.strip()
84
+
85
+ manifest["inputs"] = inputs
86
+ manifest["outputs"] = outputs
87
+
88
+ return manifest
File without changes
@@ -0,0 +1,44 @@
1
+ import inspect
2
+ from typing import Callable
3
+
4
+ from spadelib._output import read_block_manifest, write_outputs
5
+ from spadelib._scanning import build_function_args
6
+ from spadelib.secrets import _load_secrets
7
+
8
+
9
+ def run(fn: Callable) -> None:
10
+ """Execute a handler function as a Spade block.
11
+
12
+ 1. Load scalar parameters from params.yaml
13
+ 2. Scan inputs/ directory for file-based arguments
14
+ 3. Build the function arguments dict
15
+ 4. Call the handler function
16
+ 5. Write return value(s) to outputs/ directory
17
+
18
+ Args:
19
+ fn: The handler function to execute. Its type hints determine
20
+ how inputs are loaded and outputs are written.
21
+ """
22
+ # Scrub SPADE_SECRETS from the environment early (before the handler runs),
23
+ # even if the block never calls get_secret. Idempotent.
24
+ _load_secrets()
25
+
26
+ args = build_function_args(fn)
27
+
28
+ sig = inspect.signature(fn)
29
+ param_names = set(sig.parameters.keys())
30
+
31
+ has_var_keyword = any(
32
+ p.kind == inspect.Parameter.VAR_KEYWORD
33
+ for p in sig.parameters.values()
34
+ )
35
+
36
+ if has_var_keyword:
37
+ filtered_args = args
38
+ else:
39
+ filtered_args = {k: v for k, v in args.items() if k in param_names}
40
+
41
+ result = fn(**filtered_args)
42
+
43
+ manifest_outputs = read_block_manifest()
44
+ write_outputs(result, manifest_outputs)
@@ -0,0 +1,50 @@
1
+ """Access to secrets injected into a block by the Spade runtime.
2
+
3
+ Secrets are delivered as the ``SPADE_SECRETS`` environment variable — a JSON
4
+ object mapping the block's logical secret names to their values — by the worker
5
+ (cloud) or the CLI (local). This module parses that blob, serves values through
6
+ :func:`get_secret`, and scrubs the variable from the environment so it is not
7
+ inherited by any subprocess the block spawns. See ``spec/secrets.md`` §4.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ from typing import Dict, Optional
13
+
14
+ _secrets: Optional[Dict[str, str]] = None
15
+
16
+
17
+ def _parse_secrets(raw: Optional[str]) -> Dict[str, str]:
18
+ if not raw:
19
+ return {}
20
+ return json.loads(raw)
21
+
22
+
23
+ def _load_secrets() -> Dict[str, str]:
24
+ """Parse and cache ``SPADE_SECRETS``, removing it from the environment.
25
+
26
+ The variable is popped on first read so it is not inherited by subprocesses
27
+ the block spawns. Idempotent: subsequent calls return the cached mapping.
28
+ """
29
+ global _secrets
30
+ if _secrets is None:
31
+ _secrets = _parse_secrets(os.environ.pop("SPADE_SECRETS", None))
32
+ return _secrets
33
+
34
+
35
+ def get_secret(name: str) -> str:
36
+ """Return the secret bound to a logical ``name`` for this block.
37
+
38
+ The mapping from logical name to a stored secret is declared in the
39
+ pipeline (``spec/secrets.md`` §3.2); the value is injected by the worker
40
+ (cloud) or CLI (local). Raises :class:`KeyError` if the name was not
41
+ provided — a declared-but-unresolved secret is a real error, not empty.
42
+ """
43
+ secrets = _load_secrets()
44
+ try:
45
+ return secrets[name]
46
+ except KeyError:
47
+ raise KeyError(
48
+ f"secret {name!r} was not provided to this block; "
49
+ "declare it in the pipeline's 'secrets' mapping"
50
+ ) from None
@@ -0,0 +1,51 @@
1
+ from pydantic import BaseModel
2
+
3
+
4
+ class File(BaseModel):
5
+ """Base class for single-file inputs/outputs."""
6
+ path: str
7
+
8
+
9
+ class Directory(BaseModel):
10
+ """Base class for directory-based inputs/outputs."""
11
+ path: str
12
+
13
+
14
+ class RasterFile(File):
15
+ """Raster data file (e.g., GeoTIFF)."""
16
+ pass
17
+
18
+
19
+ class VectorFile(File):
20
+ """Vector data file (e.g., GeoJSON, Shapefile)."""
21
+ pass
22
+
23
+
24
+ class TabularFile(File):
25
+ """Tabular data file (e.g., CSV, Parquet)."""
26
+ pass
27
+
28
+
29
+ class JsonFile(File):
30
+ """JSON data file."""
31
+ pass
32
+
33
+
34
+ class FileCollection(BaseModel):
35
+ """Base class for a collection of files."""
36
+ paths: list[str]
37
+
38
+
39
+ class RasterFileCollection(FileCollection):
40
+ """Collection of raster data files."""
41
+ pass
42
+
43
+
44
+ class VectorFileCollection(FileCollection):
45
+ """Collection of vector data files."""
46
+ pass
47
+
48
+
49
+ class TabularFileCollection(FileCollection):
50
+ """Collection of tabular data files."""
51
+ pass