apheris-foundry-sdk 0.2.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.
Files changed (40) hide show
  1. apheris_foundry_sdk-0.2.0/CHANGELOG.md +29 -0
  2. apheris_foundry_sdk-0.2.0/LICENSE +51 -0
  3. apheris_foundry_sdk-0.2.0/MANIFEST.in +35 -0
  4. apheris_foundry_sdk-0.2.0/PKG-INFO +57 -0
  5. apheris_foundry_sdk-0.2.0/README.md +39 -0
  6. apheris_foundry_sdk-0.2.0/docs/usage.md +114 -0
  7. apheris_foundry_sdk-0.2.0/pyproject.toml +68 -0
  8. apheris_foundry_sdk-0.2.0/setup.cfg +4 -0
  9. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/__init__.py +43 -0
  10. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/__init__.py +12 -0
  11. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/app.py +175 -0
  12. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/config.py +65 -0
  13. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/contract.py +81 -0
  14. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/entrypoint.py +47 -0
  15. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/format.py +39 -0
  16. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/output.py +90 -0
  17. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/runner.py +171 -0
  18. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/cli/types.py +87 -0
  19. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/constants.py +45 -0
  20. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/describe.py +63 -0
  21. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/errors.py +100 -0
  22. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/examples/__init__.py +0 -0
  23. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/examples/basic_module.py +42 -0
  24. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/io.py +30 -0
  25. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/params/__init__.py +13 -0
  26. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/params/describe.py +29 -0
  27. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/params/introspect.py +75 -0
  28. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/params/schema.py +98 -0
  29. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/params/validate.py +63 -0
  30. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/provenance.py +69 -0
  31. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/py.typed +0 -0
  32. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/registry.py +81 -0
  33. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/task.py +226 -0
  34. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/text.py +24 -0
  35. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk/version.py +12 -0
  36. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk.egg-info/PKG-INFO +57 -0
  37. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk.egg-info/SOURCES.txt +38 -0
  38. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk.egg-info/dependency_links.txt +1 -0
  39. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk.egg-info/requires.txt +3 -0
  40. apheris_foundry_sdk-0.2.0/src/apheris_foundry_sdk.egg-info/top_level.txt +1 -0
@@ -0,0 +1,29 @@
1
+ ## v0.2.0 (2026-07-16)
2
+
3
+ ### Feat
4
+
5
+ - add SDK PyPI publishing safeguards (#41) [50c9c](https://github.com//apheris/apheris-foundry-sdk/commit/50c9c693c1388d7e1df67ecfe3a493b488ed7470)
6
+ - accept Path/FilePath/DirectoryPath and their list[] forms as input (#39) [da412](https://github.com//apheris/apheris-foundry-sdk/commit/da412350e7d23db54319effde7863c86bb9ff022)
7
+ - **[EN-6143](https://apheris.atlassian.net/browse/EN-6143)**: add netflow compatibility skill and harness pack (#32) [d028e](https://github.com//apheris/apheris-foundry-sdk/commit/d028efa92f56152dc1096aaaa16cc6f97830f343)
8
+
9
+ ## v0.1.1 (2026-07-15)
10
+
11
+ ### Fix
12
+
13
+ - raise pydantic floor to >=2.4.0 to clear regex-DoS advisory (#35) [e5280](https://github.com//apheris/apheris-foundry-sdk/commit/e528051b3b33a401392d1a8d994344050b5031ac)
14
+
15
+ ## v0.1.0 (2026-07-15)
16
+
17
+ ### Feat
18
+
19
+ - --overwrite deletes and recreates the output directory (#31) [b1088](https://github.com//apheris/apheris-foundry-sdk/commit/b1088a3767fe7cf2d57f36bdc594d1087e97cbbc)
20
+ - tiered input provenance (none/metadata/content) (#23) [9f463](https://github.com//apheris/apheris-foundry-sdk/commit/9f463359856608f2adcd272e761bda6a27475ed4)
21
+ - error.json, cancellation, --dry-run, and --log-level (#22) [8c9b7](https://github.com//apheris/apheris-foundry-sdk/commit/8c9b77fbac51f057572fa904cd9f5e266d62a05a)
22
+ - write config.json and result.json artifacts (#21) [eb1c1](https://github.com//apheris/apheris-foundry-sdk/commit/eb1c1985aa5b2522638d1f26736fbad1f4ce0573)
23
+ - describe --json self-description (#20) [b6e0a](https://github.com//apheris/apheris-foundry-sdk/commit/b6e0a654ec883b02f81d279343210588d7bc621f)
24
+ - --config flag merged into task parameters (#19) [7da3e](https://github.com//apheris/apheris-foundry-sdk/commit/7da3e28ac376d84eaa264c35a964b42b3ba7421f)
25
+ - nested params as dotted flags, JSON flags for complex fields (#18) [711ca](https://github.com//apheris/apheris-foundry-sdk/commit/711ca1c91355b2ba4c9ef7510d4a61b149a84490)
26
+ - built-in --input and --output directory flags (#17) [1c5d5](https://github.com//apheris/apheris-foundry-sdk/commit/1c5d55edabf835da6021b6ddc69ac43ce6b5631f)
27
+ - typed task parameters from any pydantic model (#16) [55c86](https://github.com//apheris/apheris-foundry-sdk/commit/55c86c3083bce2fb89883642924c8a000837352b)
28
+ - add base CLI prototype (#15) [fb1c4](https://github.com//apheris/apheris-foundry-sdk/commit/fb1c4df29e251a4a746ec418a6ad63c24223e0f8)
29
+ - EN-6069 migrate apheris-foundry-setup skill (+ k8s orchestrator mapping fixes) (#8) [59d9f](https://github.com//apheris/apheris-foundry-sdk/commit/59d9f53d225ff3c19e2b9a5236778fff4cc1c90d)
@@ -0,0 +1,51 @@
1
+ Apheris Foundry Software License
2
+ Copyright © 2025-2026 apheris AI GmbH, Kolonnenstr. 8, 10827 Berlin, Germany ("Licensor" or "Apheris"). All rights reserved.
3
+ Definitions:
4
+ "Software" means the Apheris Foundry software made available by Licensor to Licensee, in whole or in part and in any form of delivery, including the Foundry modules and container (e.g. Docker) images, the Foundry command-line interface (CLI), Python packages and wheel (.whl) distributions, workflow definitions and configuration files, the Foundry Hub orchestration service, and any accompanying libraries, scripts, skills, and documentation, together with any updates, patches, and new versions provided by Licensor.
5
+ "Commercial Agreement" means the order form, master subscription or services agreement, evaluation or design-partner agreement, memorandum of understanding, or other written agreement between Licensor and Licensee that governs Licensee's access to and use of the Software.
6
+ "Component" means any separately delivered part of the Software, for example an individual module image, the CLI, or a wheel file.
7
+ License Grant:
8
+ Subject to Licensee's compliance with this License and the Commercial Agreement, Licensor grants Licensee a non-exclusive, non-transferable, and non-sublicensable right to install, run, and use the Software for Licensee's internal business purposes, solely to the extent, for the term, in the scope, and against the fees (if any) set out in the Commercial Agreement.
9
+ Where the Commercial Agreement so provides, use may be granted free of charge, for example for evaluation, design-partner, or limited-time access. Free-of-charge use is not the default and applies only where expressly stated in writing by Licensor.
10
+ If no Commercial Agreement is in place, Licensee's use is limited to internal evaluation of Software lawfully obtained from Licensor, on a non-production and revocable basis, until a Commercial Agreement is concluded or access is withdrawn.
11
+ Restrictions:
12
+ Except as expressly permitted in the Commercial Agreement, Licensee shall not:
13
+ Redistribute, publish, sublicense, sell, rent, lease, or otherwise make the Software or any Component available to any third party, in whole or in part, with or without modification;
14
+ Pledge or encumber the rights granted under this License, or make them subject to any rights of third parties;
15
+ Reverse engineer, decompile, disassemble, or otherwise attempt to derive the source code, model weights, or underlying structure of the Software, except to the extent this restriction cannot be excluded under applicable mandatory law;
16
+ Use the Software, its outputs, or any model weights to train, fine-tune, distil, or develop any model, product, or service that competes with Licensor's products or services, or attempt model extraction, model inversion, or membership-inference attacks against the Software or its models;
17
+ Modify, adapt, translate, or create derivative works of the Software, other than configuration expressly contemplated by the Software or permitted in the Commercial Agreement;
18
+ Remove, obscure, or alter any copyright, license, or other proprietary notice in or on the Software.
19
+ Licensee must reproduce and retain all copyright and ownership notices in all permitted copies or substantial portions of the Software. Licensee acknowledges that breach of these restrictions may cause Licensor irreparable harm for which monetary damages would be inadequate, and that Licensor is entitled to seek injunctive or other equitable relief in addition to any other remedy.
20
+ Notices and Delivery of this License:
21
+ This License governs each Component whether it is delivered together with the Component, embedded within it, or referenced by it. Licensor may include this License, or a short notice referring to it, within each Component, for example as a LICENSE file inside a container image or wheel package, as CLI output, or as accompanying documentation. The absence of a License file next to a given Component does not waive or limit this License.
22
+ Third-Party and Open-Source Components:
23
+ The Software may include third-party or open-source components licensed under their own terms. Those terms are set out in the applicable NOTICE or third-party license files delivered with the Software and govern those components. Nothing in this License limits Licensee's rights, or extends Licensor's rights, under those third-party terms.
24
+ Research and Non-Clinical Use:
25
+ The Software and its outputs are provided for internal research and development only. They are not a medical device and are not intended for diagnosis, treatment, or any use that requires regulatory clearance or approval. Outputs are predictions that may be inaccurate or incomplete, and Licensee is solely responsible for independently validating them before relying on them. Licensee is responsible for controlling how its users, and any agents or automated tools it operates, interact with the Software and with Licensee's own data, including any sensitive sequences or other confidential inputs.
26
+ Models, Weights, and Outputs:
27
+ Rights in any machine-learning models, model weights, and outputs made available through or produced using the Software are governed by the Commercial Agreement. Except as expressly stated there, this License grants no rights in any models, weights, or outputs.
28
+ Feedback:
29
+ Any suggestions, ideas, or other feedback that Licensee provides regarding the Software belong exclusively to Licensor. Licensee assigns to Licensor all rights in such feedback to the extent assignable and, to the extent any right cannot be assigned, grants Licensor a perpetual, irrevocable, worldwide, royalty-free license to use it without restriction and without obligation to Licensee.
30
+ Confidentiality:
31
+ The Software, including its non-public components, structure, and performance characteristics, is confidential information of Licensor. Licensee shall protect it with at least reasonable care, shall not disclose it to third parties, and shall use it only as permitted under this License and the Commercial Agreement. This obligation does not apply to information that is or becomes public through no fault of Licensee, or that Licensee is required to disclose by law or court order, provided that Licensee gives Licensor reasonable prior notice where lawful.
32
+ Compliance, Export Control, and Data Protection:
33
+ Licensee shall comply with all applicable export control and sanctions laws and shall not export, re-export, or use the Software in violation of them. Licensee represents that any data it inputs into the Software has been lawfully obtained and that its use of such data, including any personal data under the EU General Data Protection Regulation and other applicable data protection law, complies with applicable law.
34
+ No Transfer of Ownership:
35
+ This License does not convey any ownership in the Software. The Software, and all intellectual property rights in it, remain the sole and exclusive property of Licensor and its licensors. All rights not expressly granted are reserved.
36
+ Assignment:
37
+ Licensor may assign or transfer this License, in whole or in part, including in connection with a merger, acquisition, financing, or sale of assets. Licensee may not assign or transfer this License or any rights under it, whether by operation of law, change of control, or otherwise, without Licensor's prior written consent. Any purported assignment in violation of this section is void.
38
+ Term and Termination:
39
+ This License is effective until terminated and, where use is tied to a Commercial Agreement, runs co-terminously with that agreement. During the term of an active Commercial Agreement, Licensor will not terminate this License for convenience. Either party may terminate for cause on 30 days' written notice if the other materially breaches this License or the Commercial Agreement and fails to cure the breach within that period. Where no Commercial Agreement is in place, or for free-of-charge or evaluation use, either party may terminate at any time and Licensor may withdraw access at any time. On termination, all rights granted to Licensee end immediately, and Licensee shall cease all use of the Software and delete or destroy all copies in its possession or control, except for copies retained in routine backups or as required by applicable law, which remain subject to this License.
40
+ Disclaimer and Liability:
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT SHALL LICENSOR OR ITS AFFILIATES BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, EXCEPT IN CASE OF INTENT OR GROSS NEGLIGENCE OR IN CASE OF HARM TO LIFE, BODY, OR HEALTH.
42
+ In addition, the Licensor is liable for breaches of cardinal obligations, that is, essential duties whose fulfilment enables proper performance of this License and on which Licensee may reasonably rely. In cases of ordinary negligence, such liability is limited to foreseeable, typical damages at the time of conclusion. Indirect or consequential damages are compensable only if foreseeable when using the Software as intended. Any monetary limitation or cap on liability is as set out in the Commercial Agreement; where use is free of charge and no Commercial Agreement applies, Licensor's aggregate liability is limited to the maximum extent permitted by applicable law. Mandatory statutory liability, for example under product liability law, remains unaffected.
43
+ Survival:
44
+ Termination does not affect any provision that by its nature should survive, including Restrictions, Research and Non-Clinical Use, Models, Weights, and Outputs, Feedback, Confidentiality, Compliance, Export Control, and Data Protection, No Transfer of Ownership, Assignment, Disclaimer and Liability, Precedence, and Governing Law.
45
+ Precedence:
46
+ If there is a conflict between this License and the Commercial Agreement, the Commercial Agreement prevails to the extent of the conflict.
47
+ Severability:
48
+ If a provision of this License should be held invalid, this shall not affect the validity of the remaining provisions.
49
+ Governing Law:
50
+ This License shall be governed by and construed in accordance with the laws of the Federal Republic of Germany, without regard to its conflict of law principles.
51
+ For licensing inquiries, please contact support@apheris.com.
@@ -0,0 +1,35 @@
1
+ include CHANGELOG.md
2
+ include LICENSE
3
+ include README.md
4
+ include pyproject.toml
5
+
6
+ recursive-include docs *.md
7
+ recursive-include src/apheris_foundry_sdk *.py
8
+ recursive-include src/apheris_foundry_sdk py.typed
9
+
10
+ prune .agents
11
+ prune .codex
12
+ prune .github
13
+ prune customer-packs
14
+ prune scripts
15
+ prune skills
16
+ prune tests
17
+ prune coverage_html_report
18
+
19
+ exclude .coverage
20
+ exclude .coverage_threshold
21
+ exclude .coveragerc
22
+ exclude .cz.yaml
23
+ exclude .editorconfig
24
+ exclude .flake8
25
+ exclude .gitignore
26
+ exclude .pre-commit-config.yaml
27
+ exclude .python-version
28
+ exclude coverage.xml
29
+ exclude pixi.lock
30
+ exclude pixi.toml
31
+
32
+ global-exclude *.py[cod]
33
+ global-exclude __pycache__
34
+ global-exclude scm_file_list.json
35
+ global-exclude scm_version.json
@@ -0,0 +1,57 @@
1
+ Metadata-Version: 2.4
2
+ Name: apheris-foundry-sdk
3
+ Version: 0.2.0
4
+ Summary: Reference Python SDK for Apheris Foundry per-module CLIs.
5
+ Author: Apheris
6
+ License-Expression: LicenseRef-Apheris-Foundry-Software-License
7
+ Project-URL: License, https://www.apheris.com/docs/hub/hub-license.html
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: click>=8.1
15
+ Requires-Dist: pydantic<3,>=2.4.0
16
+ Requires-Dist: pyyaml>=6
17
+ Dynamic: license-file
18
+
19
+ # Apheris Foundry SDK
20
+
21
+ Reference Python SDK for building Apheris Foundry per-module CLIs.
22
+
23
+ ## Usage
24
+
25
+ Declare a task — a plain function plus a pydantic config model — and foundry
26
+ builds a typed CLI for it. See the [usage guide](docs/usage.md) to get started.
27
+
28
+ ## Development
29
+
30
+ This project uses [pixi](https://pixi.sh) to manage the toolchain and environments.
31
+
32
+ ```bash
33
+ pixi run install-dev # install the git hooks (pre-commit + commit-msg)
34
+ pixi run test # run the test suite
35
+ pixi run lint # check formatting and lint with ruff
36
+ pixi run fix # auto-fix formatting and lint issues
37
+ pixi run build # build the wheel and sdist
38
+ ```
39
+
40
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org/); the
41
+ commit-msg hook enforces this via commitizen.
42
+
43
+ ## Project structure
44
+
45
+ ```text
46
+ src/apheris_foundry_sdk/ # SDK package
47
+ tests/ # test suite
48
+ docs/ # usage guide
49
+ ```
50
+
51
+ ## License
52
+
53
+ This package is licensed under the [Apheris Foundry Software License](https://www.apheris.com/docs/hub/hub-license.html). The full license text is also included in every published distribution as `LICENSE`.
54
+
55
+ ## Support
56
+
57
+ Contact [support@apheris.com](mailto:support@apheris.com) for access and deployment assistance.
@@ -0,0 +1,39 @@
1
+ # Apheris Foundry SDK
2
+
3
+ Reference Python SDK for building Apheris Foundry per-module CLIs.
4
+
5
+ ## Usage
6
+
7
+ Declare a task — a plain function plus a pydantic config model — and foundry
8
+ builds a typed CLI for it. See the [usage guide](docs/usage.md) to get started.
9
+
10
+ ## Development
11
+
12
+ This project uses [pixi](https://pixi.sh) to manage the toolchain and environments.
13
+
14
+ ```bash
15
+ pixi run install-dev # install the git hooks (pre-commit + commit-msg)
16
+ pixi run test # run the test suite
17
+ pixi run lint # check formatting and lint with ruff
18
+ pixi run fix # auto-fix formatting and lint issues
19
+ pixi run build # build the wheel and sdist
20
+ ```
21
+
22
+ Commits follow [Conventional Commits](https://www.conventionalcommits.org/); the
23
+ commit-msg hook enforces this via commitizen.
24
+
25
+ ## Project structure
26
+
27
+ ```text
28
+ src/apheris_foundry_sdk/ # SDK package
29
+ tests/ # test suite
30
+ docs/ # usage guide
31
+ ```
32
+
33
+ ## License
34
+
35
+ This package is licensed under the [Apheris Foundry Software License](https://www.apheris.com/docs/hub/hub-license.html). The full license text is also included in every published distribution as `LICENSE`.
36
+
37
+ ## Support
38
+
39
+ Contact [support@apheris.com](mailto:support@apheris.com) for access and deployment assistance.
@@ -0,0 +1,114 @@
1
+ # Usage guide
2
+
3
+ Build a per-module CLI by declaring tasks. A task is a plain function plus a
4
+ pydantic config model — no foundry base class.
5
+
6
+ ## Define a task
7
+
8
+ ```python
9
+ from pydantic import BaseModel, DirectoryPath, Field
10
+ from apheris_foundry_sdk import run_foundry_cli, task
11
+
12
+
13
+ class PredictConfig(BaseModel):
14
+ model: str = Field("default", description="Model identifier")
15
+ precision: str = "fp32"
16
+
17
+
18
+ class PredictResult(BaseModel):
19
+ model: str
20
+
21
+
22
+ @task("predict", description="Run a prediction")
23
+ def predict(input: list[DirectoryPath], output: DirectoryPath, config: PredictConfig) -> PredictResult:
24
+ return PredictResult(model=config.model)
25
+
26
+
27
+ def main() -> int:
28
+ return run_foundry_cli(module_name="apheris-example", module_version="0.1.0")
29
+ ```
30
+
31
+ Every task takes three arguments and returns a model — the signature is checked
32
+ when the task is declared:
33
+
34
+ - `input` — annotate it with the shape you want; `--input` follows:
35
+
36
+ | Annotation | `--input` | task receives |
37
+ |------------|-----------|---------------|
38
+ | `DirectoryPath` | one directory | a `Path` |
39
+ | `FilePath` | one file | a `Path` |
40
+ | `Path` | one file or directory | a `Path` |
41
+ | `list[DirectoryPath]` | repeatable directories | a `list[Path]` |
42
+ | `list[FilePath]` | repeatable files | a `list[Path]` |
43
+ | `list[Path]` | repeatable files or directories | a `list[Path]` |
44
+
45
+ The kind (`FilePath`/`DirectoryPath`/`Path`) maps to what `--input` accepts;
46
+ the `list[...]` form makes it repeatable. Anything else (a union, a `tuple`)
47
+ is rejected at declaration. click validates each path exists and is the right
48
+ kind — a file given to a directory-only task (or vice versa) is a precondition
49
+ error (exit 4). A single-input task rejects a repeated `--input` as a usage
50
+ error, rather than silently keeping the last.
51
+ - `output: DirectoryPath` — the `--output` directory (foundry creates it).
52
+ - `config: <BaseModel>` — your config model; each field becomes a flag.
53
+ - returns a pydantic `BaseModel` (written to `result.json`), or `None` for a
54
+ side-effect-only task.
55
+
56
+ ## Run it
57
+
58
+ ```console
59
+ $ apheris-example predict --input ./in --output ./run --model demo
60
+ ```
61
+
62
+ - Config fields become flags: `--model`, `--precision`, etc. Nested models use
63
+ dotted flags (`--optimizer.lr`); complex fields take inline JSON.
64
+ - `--config` sets values in bulk from a JSON/YAML file or inline JSON; explicit flags win.
65
+ - `--overwrite` deletes and recreates a non-empty output directory (only one this tool
66
+ created), so the artifacts reflect the latest run alone.
67
+ - `--dry-run` validates and writes `config.json`, without running the task.
68
+ - `--log-level debug` prints a stack trace on failure.
69
+
70
+ ## Inspect a task
71
+
72
+ ```console
73
+ $ apheris-example describe # module + its tasks
74
+ $ apheris-example predict describe # one task; add --json for machine output
75
+ ```
76
+
77
+ `describe` needs no `--input`/`--output` — only running the task does.
78
+
79
+ ## Artifacts
80
+
81
+ Each run writes to the output directory:
82
+
83
+ - `config.json` — the resolved config.
84
+ - `result.json` — the returned model (dumped to JSON) plus a provenance block.
85
+ - `error.json` — written instead when the task fails.
86
+
87
+ ## Input provenance
88
+
89
+ `result.json` records where each input came from. How much is recorded is a per-task
90
+ choice, declared at `@task` — hashing every byte on every run is costly for large
91
+ input roots, so it's opt-in:
92
+
93
+ ```python
94
+ @task("predict", input_provenance="metadata")
95
+ def predict(...): ...
96
+ ```
97
+
98
+ | Tier | Records | Cost |
99
+ |------|---------|------|
100
+ | `none` (default) | path only | no traversal |
101
+ | `metadata` | digest over each file's path/size/mtime | stat only |
102
+ | `content` | digest over each file's bytes | reads everything |
103
+
104
+ ## Exit codes
105
+
106
+ | Code | Category | Meaning |
107
+ |------|----------|---------|
108
+ | 0 | success | ran successfully |
109
+ | 1 | internal | unexpected/uncaught error |
110
+ | 2 | usage | bad invocation or config |
111
+ | 3 | input-data | invalid input data |
112
+ | 4 | precondition | a required resource is missing |
113
+ | 5 | output | could not write output |
114
+ | 130 | cancelled | interrupted (SIGINT/SIGTERM) |
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["setuptools>=80", "wheel", "setuptools_scm[toml]>=8"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "apheris-foundry-sdk"
7
+ description = "Reference Python SDK for Apheris Foundry per-module CLIs."
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ authors = [{ name = "Apheris" }]
11
+ license = "LicenseRef-Apheris-Foundry-Software-License"
12
+ license-files = ["LICENSE"]
13
+ dependencies = ["click>=8.1", "pydantic>=2.4.0,<3", "pyyaml>=6"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ ]
19
+ dynamic = ["version"]
20
+
21
+ [project.urls]
22
+ License = "https://www.apheris.com/docs/hub/hub-license.html"
23
+
24
+ [tool.setuptools_scm]
25
+
26
+ [tool.setuptools.package-data]
27
+ apheris_foundry_sdk = ["py.typed"]
28
+
29
+ [tool.setuptools.package-dir]
30
+ "" = "src"
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+ include = ["apheris_foundry_sdk*"]
35
+
36
+ [tool.pytest.ini_options]
37
+ testpaths = ["tests"]
38
+
39
+ [tool.ruff]
40
+ line-length = 120
41
+ target-version = "py311"
42
+
43
+ [tool.ruff.lint]
44
+ select = ["ALL"]
45
+ ignore = [
46
+ "A001",
47
+ "A002",
48
+ "ANN401",
49
+ "COM812",
50
+ "S311",
51
+ "PLR0912",
52
+ "PLR0913",
53
+ "PLR0915",
54
+ "C901",
55
+ "D",
56
+ ]
57
+
58
+ [tool.ruff.lint.per-file-ignores]
59
+ "tests/*" = [
60
+ "S101", # Allow asserts in tests
61
+ "FBT001", # Boolean traps are not relevant here
62
+ "ANN", # Test methods and local fixtures don't need full annotations
63
+ "ARG001", # Test task fixtures keep SDK-required signatures
64
+ "PLR2004", # Asserting against literal exit codes/values is the point in tests
65
+ ]
66
+ "customer-packs/foundry-module-local-harness/tasks/example-module-mock/run.py" = [
67
+ "INP001", # Standalone container entrypoint, not a package module
68
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,43 @@
1
+ """Apheris Foundry SDK: reference toolkit for per-module CLIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from apheris_foundry_sdk.cli import (
6
+ FoundryGroup,
7
+ build_click_app,
8
+ build_task_command,
9
+ run_foundry_cli,
10
+ )
11
+ from apheris_foundry_sdk.constants import ModuleInfo
12
+ from apheris_foundry_sdk.errors import (
13
+ CancelledError,
14
+ FoundryError,
15
+ InputDataError,
16
+ InternalError,
17
+ OutputError,
18
+ PreconditionError,
19
+ UsageError,
20
+ )
21
+ from apheris_foundry_sdk.registry import Registry, default_registry, tasks
22
+ from apheris_foundry_sdk.task import task
23
+ from apheris_foundry_sdk.version import __version__
24
+
25
+ __all__ = [
26
+ "CancelledError",
27
+ "FoundryError",
28
+ "FoundryGroup",
29
+ "InputDataError",
30
+ "InternalError",
31
+ "ModuleInfo",
32
+ "OutputError",
33
+ "PreconditionError",
34
+ "Registry",
35
+ "UsageError",
36
+ "__version__",
37
+ "build_click_app",
38
+ "build_task_command",
39
+ "default_registry",
40
+ "run_foundry_cli",
41
+ "task",
42
+ "tasks",
43
+ ]
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from .app import build_click_app, build_task_command
4
+ from .contract import FoundryGroup
5
+ from .entrypoint import run_foundry_cli
6
+
7
+ __all__ = [
8
+ "FoundryGroup",
9
+ "build_click_app",
10
+ "build_task_command",
11
+ "run_foundry_cli",
12
+ ]
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ import click
7
+
8
+ from apheris_foundry_sdk.constants import ModuleInfo
9
+ from apheris_foundry_sdk.describe import describe_module, describe_task
10
+
11
+ from .contract import FoundryGroup
12
+ from .format import format_module, format_task
13
+ from .runner import run_task
14
+ from .types import JSON, ExplicitBool, build_path, click_type_for
15
+
16
+ if TYPE_CHECKING:
17
+ from apheris_foundry_sdk.registry import Registry, TaskSpec
18
+
19
+
20
+ def build_click_app(module: ModuleInfo, registry: Registry) -> click.Group:
21
+ """Build the branded ``apheris-<module>`` command group.
22
+
23
+ Wires up the group-level ``--version``/``--help``, a module-level ``describe``,
24
+ and each task's pre-built sub-command (each of which also has ``describe``). The
25
+ module is stored on the click context so ``describe`` can read its name.
26
+ """
27
+
28
+ @click.group(cls=FoundryGroup, name=module.name, context_settings={"help_option_names": ["--help"]})
29
+ @click.version_option(
30
+ version=f"{module.version} cli-contract {module.cli_contract_version}",
31
+ prog_name=module.name,
32
+ message="%(prog)s %(version)s",
33
+ )
34
+ @click.pass_context
35
+ def app(ctx: click.Context) -> None:
36
+ ctx.obj = module
37
+
38
+ @app.command(name="describe")
39
+ @click.option("--json", "as_json", is_flag=True, help="Emit the machine-readable JSON description.")
40
+ def module_describe(as_json: bool) -> None: # noqa: FBT001 - Click injects flags positionally.
41
+ payload = describe_module(registry, module.name, module.version, module.cli_contract_version)
42
+ click.echo(json.dumps(payload, indent=2, sort_keys=True) if as_json else format_module(payload))
43
+
44
+ for spec in registry.tasks().values():
45
+ app.add_command(spec.command)
46
+ return app
47
+
48
+
49
+ def build_task_command(spec: TaskSpec) -> FoundryGroup:
50
+ """Build the task's command (a :class:`FoundryGroup`).
51
+
52
+ Built once at registration and stored on the spec. It is a group (not a bare
53
+ command) so it can carry its own ``describe`` sub-command while still running
54
+ the task itself via ``invoke_without_command``. As a :class:`FoundryGroup` it
55
+ carries the error contract whether run standalone or under the module group.
56
+ """
57
+ params: list[click.Parameter] = [*_foundry_options(spec), *_param_options(spec)]
58
+
59
+ @click.pass_context
60
+ def callback(ctx: click.Context, **kwargs: Any) -> int | None:
61
+ if ctx.invoked_subcommand is not None:
62
+ return None
63
+ return run_task(spec, module_from_context(ctx, spec), kwargs)
64
+
65
+ group = FoundryGroup(
66
+ spec.name,
67
+ params=params,
68
+ callback=callback,
69
+ help=spec.description,
70
+ invoke_without_command=True,
71
+ context_settings={"help_option_names": ["--help"]},
72
+ )
73
+
74
+ @group.command(name="describe")
75
+ @click.option("--json", "as_json", is_flag=True, help="Emit the machine-readable JSON description.")
76
+ @click.pass_context
77
+ def task_describe(ctx: click.Context, as_json: bool) -> None: # noqa: FBT001 - Click injects flags positionally.
78
+ payload = describe_task(spec, module_from_context(ctx, spec).name)
79
+ click.echo(json.dumps(payload, indent=2, sort_keys=True) if as_json else format_task(payload))
80
+
81
+ return group
82
+
83
+
84
+ def module_from_context(ctx: click.Context, spec: TaskSpec) -> ModuleInfo:
85
+ """The module a task runs under, taken from the click context.
86
+
87
+ ``build_click_app`` stores the real :class:`ModuleInfo` on the context. A task
88
+ command run on its own (no module group above it) has none, so fall back to a
89
+ module named after the task itself.
90
+ """
91
+ root = ctx.find_root()
92
+ if isinstance(root.obj, ModuleInfo):
93
+ return root.obj
94
+ return ModuleInfo(name=spec.name, version="0.0.0")
95
+
96
+
97
+ def _at_most_once(_ctx: click.Context, _param: click.Parameter, values: tuple[Any, ...]) -> Any:
98
+ """Unwrap a repeatable flag to a single value, rejecting more than one."""
99
+ if len(values) > 1:
100
+ msg = "--input may only be given once for this task"
101
+ raise click.UsageError(msg)
102
+ return values[0] if values else None
103
+
104
+
105
+ def _foundry_options(spec: TaskSpec) -> list[click.Parameter]:
106
+ """The built-in flags: ``--input`` (file or directory, repeatable for a list), ``--output``, ``--config``."""
107
+ noun = spec.input.noun
108
+ return [
109
+ # --input/--output are required, but not via click's required=True: that fires
110
+ # during parsing even for `<task> describe`, which needs neither. The runner
111
+ # enforces presence only when the task actually runs.
112
+ click.Option(
113
+ ["--input", "input"],
114
+ type=build_path(is_file=spec.input.is_file, is_dir=spec.input.is_dir, must_exist=True),
115
+ multiple=True,
116
+ callback=_at_most_once if not spec.input.multiple else None,
117
+ default=(),
118
+ help=f"Input {noun} (repeatable)." if spec.input.multiple else f"Input {noun}.",
119
+ ),
120
+ click.Option(
121
+ ["--output", "output"],
122
+ # Output dir need not exist (the runner creates it); must be writable.
123
+ type=build_path(is_file=False, is_dir=True, must_exist=False, writable=True),
124
+ default=None,
125
+ help="Output directory (required).",
126
+ ),
127
+ click.Option(
128
+ ["--overwrite"],
129
+ type=ExplicitBool(),
130
+ default=None,
131
+ required=False,
132
+ help="Delete and recreate a non-empty output directory (only if this tool created it).",
133
+ ),
134
+ click.Option(
135
+ ["--dry-run", "dry_run"],
136
+ type=ExplicitBool(),
137
+ default=None,
138
+ required=False,
139
+ help="Validate inputs and config and write config.json, without running the task.",
140
+ ),
141
+ click.Option(
142
+ ["--log-level", "log_level"],
143
+ type=click.Choice(["info", "debug"]),
144
+ default="info",
145
+ required=False,
146
+ help="Log level; 'debug' prints a stack trace on failure ('info' is the default).",
147
+ ),
148
+ click.Option(
149
+ ["--config"],
150
+ type=str,
151
+ default=None,
152
+ required=False,
153
+ help="Parameter values as inline JSON or a path to a JSON file; flags override config.",
154
+ ),
155
+ ]
156
+
157
+
158
+ def _param_options(spec: TaskSpec) -> list[click.Parameter]:
159
+ """One flag per config field, from the params introspected at registration."""
160
+ options: list[click.Parameter] = []
161
+ for param in spec.params.values():
162
+ # Scalar leaves get a typed flag (repeatable for a list); complex-but-JSON-able
163
+ # leaves (dict, list of models, unions) get a single inline-JSON flag.
164
+ multiple = param.scalar and param.repeatable
165
+ options.append(
166
+ click.Option(
167
+ [param.flag, param.dest],
168
+ type=click_type_for(param.element) if param.scalar else JSON(),
169
+ multiple=multiple,
170
+ default=() if multiple else None,
171
+ required=False,
172
+ help=param.description or ("" if param.scalar else "Inline JSON value."),
173
+ ),
174
+ )
175
+ return options