toolfuncs 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,73 @@
1
+ name: Publish release
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ build:
9
+ name: Build distributions
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: read
13
+
14
+ steps:
15
+ - name: Check out the release tag
16
+ uses: actions/checkout@v6
17
+ with:
18
+ persist-credentials: false
19
+
20
+ - name: Set up Python
21
+ uses: actions/setup-python@v6
22
+ with:
23
+ python-version: "3.x"
24
+
25
+ - name: Verify that the tag matches the package version
26
+ env:
27
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
28
+ run: |
29
+ python - <<'PY'
30
+ import os
31
+ import pathlib
32
+ import tomllib
33
+
34
+ version = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"]
35
+ expected_tag = f"v{version}"
36
+ if os.environ["RELEASE_TAG"] != expected_tag:
37
+ raise SystemExit(
38
+ f"release tag {os.environ['RELEASE_TAG']!r} does not match {expected_tag!r}"
39
+ )
40
+ PY
41
+
42
+ - name: Install the build frontend
43
+ run: python -m pip install --disable-pip-version-check build
44
+
45
+ - name: Build the wheel and source distribution
46
+ run: python -m build
47
+
48
+ - name: Store the distributions
49
+ uses: actions/upload-artifact@v5
50
+ with:
51
+ name: python-package-distributions
52
+ path: dist/
53
+ if-no-files-found: error
54
+
55
+ publish:
56
+ name: Publish to PyPI
57
+ needs: build
58
+ runs-on: ubuntu-latest
59
+ environment:
60
+ name: pypi
61
+ url: https://pypi.org/project/toolfuncs/
62
+ permissions:
63
+ id-token: write
64
+
65
+ steps:
66
+ - name: Download the distributions
67
+ uses: actions/download-artifact@v6
68
+ with:
69
+ name: python-package-distributions
70
+ path: dist/
71
+
72
+ - name: Publish the distributions
73
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,9 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .coverage
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ .basedpyright/
7
+ build/
8
+ dist/
9
+ *.egg-info/
@@ -0,0 +1 @@
1
+ 3.11
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.5
2
+ Name: toolfuncs
3
+ Version: 0.1.0
4
+ Summary: Function-first Python tools with matching import and command-line interfaces
5
+ Project-URL: Repository, https://github.com/nimashoghi/toolfuncs
6
+ Project-URL: Issues, https://github.com/nimashoghi/toolfuncs/issues
7
+ Author-email: Nima Shoghi <nima@boltz.bio>
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: cyclopts<5,>=4.23.2
10
+ Requires-Dist: fsspec[github,http]>=2025.3.0
11
+ Requires-Dist: packaging>=24
12
+ Requires-Dist: pip>=25
13
+ Requires-Dist: pydantic-core<3,>=2.20
14
+ Description-Content-Type: text/markdown
15
+
16
+ # toolfuncs
17
+
18
+ `toolfuncs` is a small function-calling convention for Python tools. A tool is a Python module whose typed public functions are its interface. The same functions can be called directly from Python or projected into a command-line interface without maintaining a second wrapper API.
19
+
20
+ Discoverable tools live in a project or user-wide tool directory in one of two forms: a PEP 723 Python file or one packaged `src` project layout. The separate `import_path()` utility can load broader Python sources from local paths and fsspec URLs.
21
+
22
+ ## The basic model
23
+
24
+ Write ordinary typed functions and register the CLI-callable operations on `toolfuncs.App`:
25
+
26
+ ```python
27
+ #!/usr/bin/env -S uv run --script
28
+ # /// script
29
+ # requires-python = ">=3.11"
30
+ # dependencies = ["toolfuncs"]
31
+ # [tool.toolfuncs]
32
+ # description = "Render values for another program."
33
+ # ///
34
+
35
+ from pathlib import Path
36
+
37
+ from toolfuncs import App
38
+
39
+ app = App()
40
+
41
+
42
+ @app.command
43
+ def render(value: str, *, output: Path | None = None) -> dict[str, object]:
44
+ """Return a value and its optional output path."""
45
+
46
+ return {"value": value, "output": output}
47
+
48
+
49
+ if __name__ == "__main__":
50
+ app()
51
+ ```
52
+
53
+ The Python interface is the definition:
54
+
55
+ ```python
56
+ from pathlib import Path
57
+
58
+ from toolfuncs import renderer
59
+
60
+ result = renderer.render("hello", output=Path("result.txt"))
61
+ ```
62
+
63
+ The CLI is derived from that same signature and returns strict JSON:
64
+
65
+ ```console
66
+ $ toolfuncs renderer render hello --output result.txt
67
+ {"value": "hello", "output": "result.txt"}
68
+ ```
69
+
70
+ `toolfuncs` directly depends on Cyclopts and `pydantic-core`. Its `App` is a Cyclopts app with the established strict-JSON result action as its default, so a tool imports only `toolfuncs` rather than importing and configuring those libraries itself. `Parameter`, `Token`, `Group`, `validators`, `types`, and `to_jsonable_python` are also re-exported for tools that need the corresponding advanced behavior. Passing `result_action=` explicitly preserves Cyclopts' ordinary override behavior for streaming or domain-specific status policies.
71
+
72
+ The decorator registers the original function without wrapping it. Direct Python calls therefore receive the original object and exceptions. CLI calls parse annotated values, emit one JSON value on stdout, and fail nonzero when parsing, execution, or serialization fails.
73
+
74
+ ## Scoped tools
75
+
76
+ `toolfuncs` discovers tools from two roots:
77
+
78
+ 1. the nearest `.agents/tools` directory at or above the process's initial working directory;
79
+ 2. `~/.agents/tools`.
80
+
81
+ An immediate child defines a tool only when it has one of these exact shapes:
82
+
83
+ ```text
84
+ name.py # PEP 723 script metadata required
85
+
86
+ name/
87
+ pyproject.toml
88
+ src/
89
+ name/
90
+ __init__.py
91
+ ```
92
+
93
+ Both metadata containers require one static field:
94
+
95
+ ```toml
96
+ [tool.toolfuncs]
97
+ description = "Describe this tool in one line."
98
+ ```
99
+
100
+ For the file form, that table is inside the PEP 723 `script` block. For the packaged form, `pyproject.toml` must also define a matching normalized `[project].name` and an explicit `[build-system]`. The tool directory name, scoped name, and Python import name are the same valid non-keyword identifier. Flat projects, direct package directories, single-module projects, namespace roots, legacy `setup.py` projects, artifacts, and remote URLs are not discoverable tool forms.
101
+
102
+ A project tool shadows a user tool with the same name. The roots are captured when `toolfuncs` is first imported, so a long-running interpreter does not silently change its tool universe after `chdir()`.
103
+
104
+ This supports the portable dynamic-import form:
105
+
106
+ ```python
107
+ from toolfuncs import codexr
108
+
109
+ agents = codexr.list_agents()
110
+ ```
111
+
112
+ `from toolfuncs import name` is intentionally the portable form. `import toolfuncs.name` is not promised because scoped resolution is a dynamic package attribute rather than an installed `toolfuncs` submodule. Use `load_tool("name")` when a tool name conflicts with a `toolfuncs` API member.
113
+
114
+ Universal CLI dispatch always works after installing `toolfuncs`:
115
+
116
+ ```console
117
+ toolfuncs codexr list-agents
118
+ toolfuncs run codexr list-agents
119
+ ```
120
+
121
+ Run `toolfuncs sync` to add direct command shims for all currently visible tools to `~/.local/bin`, or choose another directory with `--bin-dir`. A shim performs scoped lookup again when invoked, so the same command correctly resolves project overrides. `sync` never overwrites an unmanaged command and does not remove older managed shims.
122
+
123
+ ## Composing tools
124
+
125
+ A tool can import another scoped tool and call it like any Python module:
126
+
127
+ ```python
128
+ from toolfuncs import codexr
129
+ from toolfuncs import App
130
+
131
+ app = App()
132
+
133
+
134
+ @app.command
135
+ def active_agent_ids() -> list[str]:
136
+ return [agent.session_id for agent in codexr.list_agents()]
137
+ ```
138
+
139
+ Imported or undecorated functions are not exposed accidentally. Only functions registered on `app` become CLI commands; `__all__` retains its ordinary Python export meaning but does not define the command surface.
140
+
141
+ ## Importing arbitrary sources
142
+
143
+ `import_path()` handles the following v1 source forms:
144
+
145
+ | Source | Behavior |
146
+ | --- | --- |
147
+ | `tool.py` | executes directly; reads optional PEP 723 metadata |
148
+ | `package/__init__.py` | imports `package` directly |
149
+ | `package/` containing `__init__.py` | imports directly with relative imports and resources |
150
+ | project containing `pyproject.toml` | builds one wheel, installs it, then imports its selected module |
151
+ | project containing `setup.py` | builds one wheel through pip's legacy-project support |
152
+ | `.whl` | inspects, installs, and imports the wheel |
153
+ | source archive such as `.tar.gz` or `.zip` | builds, installs, and imports one wheel |
154
+ | any of the above behind an fsspec URL | materializes the complete source and applies the same classification |
155
+
156
+ Direct packages may declare dependencies in an adjacent `pyproject.toml` `[project]` table. PEP 723 metadata is read only from single Python files. Projects and artifacts leave dependency handling to their build metadata and pip.
157
+
158
+ Regular projects are interpreted by their build backend rather than guessed from their directory tree. The resulting wheel is inspected for actual top-level Python imports. This supports conventional flat and `src` layouts for both modules and packages, as well as installed namespace packages. A bare `src/__init__.py` is not treated as a package layout.
159
+
160
+ Import selection follows this order:
161
+
162
+ 1. explicit `import_name=`;
163
+ 2. a project directory name that matches an import in the built wheel;
164
+ 3. the wheel's sole top-level import.
165
+
166
+ Ambiguous projects fail with their discovered candidates:
167
+
168
+ ```python
169
+ from toolfuncs import import_path
170
+
171
+ pillow = import_path("./vendor/Pillow", import_name="PIL")
172
+ ```
173
+
174
+ `import_name=` belongs only to this generic utility. Discoverable packaged tools never configure or infer their import name; the scoped name is passed to `import_path()` internally.
175
+
176
+ ## Identity and execution environment
177
+
178
+ Within one interpreter, a canonical source and import name identify one module object. Re-importing the same source returns that object, one source cannot be assigned two names, and one name cannot be assigned two sources. Direct dotted imports construct missing namespace parents, support relative imports, clean up after failed execution, and work with `importlib.reload()`.
179
+
180
+ V1 installs dependencies and project wheels into the running interpreter environment under a process-wide import lock. This is deliberate: direct Python calls must receive the real module and Python objects rather than a proxy transport. It also means dependency conflicts are ordinary environment conflicts. Use a dedicated environment when mutually incompatible tools must coexist.
181
+
182
+ Credentials in source URLs and opaque URL queries are removed from user-facing diagnostics. `storage_options=` is passed directly to fsspec and is never incorporated into module names.
183
+
184
+ ## Management commands
185
+
186
+ ```console
187
+ toolfuncs list
188
+ toolfuncs describe TOOL
189
+ toolfuncs sync [--bin-dir DIRECTORY]
190
+ toolfuncs TOOL [ARGS...]
191
+ ```
192
+
193
+ `list`, `describe`, and `sync` emit strict JSON. `list` and `describe` read only static TOML metadata and never import tools, resolve their dependencies, or run build code. Their records contain `name`, `path`, `scope`, `kind`, and `description`. Use `toolfuncs TOOL --help` when runtime command inspection is needed.
194
+
195
+ ## Development
196
+
197
+ ```console
198
+ uv sync
199
+ uv run pytest
200
+ uv run ruff format --check .
201
+ uv run ruff check .
202
+ uv run basedpyright
203
+ ```
204
+
205
+ The detailed v1 guarantees and non-goals are recorded in [the contract](docs/contract.md).
206
+
207
+ ## Releasing
208
+
209
+ The GitHub Release is the release control point. Set `[project].version`, merge and push that commit, then publish a GitHub Release whose tag is `v<version>`. The release workflow verifies that the tag and package version match, builds the wheel and source distribution, and publishes them to PyPI through the `pypi` environment and PyPI Trusted Publishing. It uses no long-lived PyPI token or repository secret.
@@ -0,0 +1,194 @@
1
+ # toolfuncs
2
+
3
+ `toolfuncs` is a small function-calling convention for Python tools. A tool is a Python module whose typed public functions are its interface. The same functions can be called directly from Python or projected into a command-line interface without maintaining a second wrapper API.
4
+
5
+ Discoverable tools live in a project or user-wide tool directory in one of two forms: a PEP 723 Python file or one packaged `src` project layout. The separate `import_path()` utility can load broader Python sources from local paths and fsspec URLs.
6
+
7
+ ## The basic model
8
+
9
+ Write ordinary typed functions and register the CLI-callable operations on `toolfuncs.App`:
10
+
11
+ ```python
12
+ #!/usr/bin/env -S uv run --script
13
+ # /// script
14
+ # requires-python = ">=3.11"
15
+ # dependencies = ["toolfuncs"]
16
+ # [tool.toolfuncs]
17
+ # description = "Render values for another program."
18
+ # ///
19
+
20
+ from pathlib import Path
21
+
22
+ from toolfuncs import App
23
+
24
+ app = App()
25
+
26
+
27
+ @app.command
28
+ def render(value: str, *, output: Path | None = None) -> dict[str, object]:
29
+ """Return a value and its optional output path."""
30
+
31
+ return {"value": value, "output": output}
32
+
33
+
34
+ if __name__ == "__main__":
35
+ app()
36
+ ```
37
+
38
+ The Python interface is the definition:
39
+
40
+ ```python
41
+ from pathlib import Path
42
+
43
+ from toolfuncs import renderer
44
+
45
+ result = renderer.render("hello", output=Path("result.txt"))
46
+ ```
47
+
48
+ The CLI is derived from that same signature and returns strict JSON:
49
+
50
+ ```console
51
+ $ toolfuncs renderer render hello --output result.txt
52
+ {"value": "hello", "output": "result.txt"}
53
+ ```
54
+
55
+ `toolfuncs` directly depends on Cyclopts and `pydantic-core`. Its `App` is a Cyclopts app with the established strict-JSON result action as its default, so a tool imports only `toolfuncs` rather than importing and configuring those libraries itself. `Parameter`, `Token`, `Group`, `validators`, `types`, and `to_jsonable_python` are also re-exported for tools that need the corresponding advanced behavior. Passing `result_action=` explicitly preserves Cyclopts' ordinary override behavior for streaming or domain-specific status policies.
56
+
57
+ The decorator registers the original function without wrapping it. Direct Python calls therefore receive the original object and exceptions. CLI calls parse annotated values, emit one JSON value on stdout, and fail nonzero when parsing, execution, or serialization fails.
58
+
59
+ ## Scoped tools
60
+
61
+ `toolfuncs` discovers tools from two roots:
62
+
63
+ 1. the nearest `.agents/tools` directory at or above the process's initial working directory;
64
+ 2. `~/.agents/tools`.
65
+
66
+ An immediate child defines a tool only when it has one of these exact shapes:
67
+
68
+ ```text
69
+ name.py # PEP 723 script metadata required
70
+
71
+ name/
72
+ pyproject.toml
73
+ src/
74
+ name/
75
+ __init__.py
76
+ ```
77
+
78
+ Both metadata containers require one static field:
79
+
80
+ ```toml
81
+ [tool.toolfuncs]
82
+ description = "Describe this tool in one line."
83
+ ```
84
+
85
+ For the file form, that table is inside the PEP 723 `script` block. For the packaged form, `pyproject.toml` must also define a matching normalized `[project].name` and an explicit `[build-system]`. The tool directory name, scoped name, and Python import name are the same valid non-keyword identifier. Flat projects, direct package directories, single-module projects, namespace roots, legacy `setup.py` projects, artifacts, and remote URLs are not discoverable tool forms.
86
+
87
+ A project tool shadows a user tool with the same name. The roots are captured when `toolfuncs` is first imported, so a long-running interpreter does not silently change its tool universe after `chdir()`.
88
+
89
+ This supports the portable dynamic-import form:
90
+
91
+ ```python
92
+ from toolfuncs import codexr
93
+
94
+ agents = codexr.list_agents()
95
+ ```
96
+
97
+ `from toolfuncs import name` is intentionally the portable form. `import toolfuncs.name` is not promised because scoped resolution is a dynamic package attribute rather than an installed `toolfuncs` submodule. Use `load_tool("name")` when a tool name conflicts with a `toolfuncs` API member.
98
+
99
+ Universal CLI dispatch always works after installing `toolfuncs`:
100
+
101
+ ```console
102
+ toolfuncs codexr list-agents
103
+ toolfuncs run codexr list-agents
104
+ ```
105
+
106
+ Run `toolfuncs sync` to add direct command shims for all currently visible tools to `~/.local/bin`, or choose another directory with `--bin-dir`. A shim performs scoped lookup again when invoked, so the same command correctly resolves project overrides. `sync` never overwrites an unmanaged command and does not remove older managed shims.
107
+
108
+ ## Composing tools
109
+
110
+ A tool can import another scoped tool and call it like any Python module:
111
+
112
+ ```python
113
+ from toolfuncs import codexr
114
+ from toolfuncs import App
115
+
116
+ app = App()
117
+
118
+
119
+ @app.command
120
+ def active_agent_ids() -> list[str]:
121
+ return [agent.session_id for agent in codexr.list_agents()]
122
+ ```
123
+
124
+ Imported or undecorated functions are not exposed accidentally. Only functions registered on `app` become CLI commands; `__all__` retains its ordinary Python export meaning but does not define the command surface.
125
+
126
+ ## Importing arbitrary sources
127
+
128
+ `import_path()` handles the following v1 source forms:
129
+
130
+ | Source | Behavior |
131
+ | --- | --- |
132
+ | `tool.py` | executes directly; reads optional PEP 723 metadata |
133
+ | `package/__init__.py` | imports `package` directly |
134
+ | `package/` containing `__init__.py` | imports directly with relative imports and resources |
135
+ | project containing `pyproject.toml` | builds one wheel, installs it, then imports its selected module |
136
+ | project containing `setup.py` | builds one wheel through pip's legacy-project support |
137
+ | `.whl` | inspects, installs, and imports the wheel |
138
+ | source archive such as `.tar.gz` or `.zip` | builds, installs, and imports one wheel |
139
+ | any of the above behind an fsspec URL | materializes the complete source and applies the same classification |
140
+
141
+ Direct packages may declare dependencies in an adjacent `pyproject.toml` `[project]` table. PEP 723 metadata is read only from single Python files. Projects and artifacts leave dependency handling to their build metadata and pip.
142
+
143
+ Regular projects are interpreted by their build backend rather than guessed from their directory tree. The resulting wheel is inspected for actual top-level Python imports. This supports conventional flat and `src` layouts for both modules and packages, as well as installed namespace packages. A bare `src/__init__.py` is not treated as a package layout.
144
+
145
+ Import selection follows this order:
146
+
147
+ 1. explicit `import_name=`;
148
+ 2. a project directory name that matches an import in the built wheel;
149
+ 3. the wheel's sole top-level import.
150
+
151
+ Ambiguous projects fail with their discovered candidates:
152
+
153
+ ```python
154
+ from toolfuncs import import_path
155
+
156
+ pillow = import_path("./vendor/Pillow", import_name="PIL")
157
+ ```
158
+
159
+ `import_name=` belongs only to this generic utility. Discoverable packaged tools never configure or infer their import name; the scoped name is passed to `import_path()` internally.
160
+
161
+ ## Identity and execution environment
162
+
163
+ Within one interpreter, a canonical source and import name identify one module object. Re-importing the same source returns that object, one source cannot be assigned two names, and one name cannot be assigned two sources. Direct dotted imports construct missing namespace parents, support relative imports, clean up after failed execution, and work with `importlib.reload()`.
164
+
165
+ V1 installs dependencies and project wheels into the running interpreter environment under a process-wide import lock. This is deliberate: direct Python calls must receive the real module and Python objects rather than a proxy transport. It also means dependency conflicts are ordinary environment conflicts. Use a dedicated environment when mutually incompatible tools must coexist.
166
+
167
+ Credentials in source URLs and opaque URL queries are removed from user-facing diagnostics. `storage_options=` is passed directly to fsspec and is never incorporated into module names.
168
+
169
+ ## Management commands
170
+
171
+ ```console
172
+ toolfuncs list
173
+ toolfuncs describe TOOL
174
+ toolfuncs sync [--bin-dir DIRECTORY]
175
+ toolfuncs TOOL [ARGS...]
176
+ ```
177
+
178
+ `list`, `describe`, and `sync` emit strict JSON. `list` and `describe` read only static TOML metadata and never import tools, resolve their dependencies, or run build code. Their records contain `name`, `path`, `scope`, `kind`, and `description`. Use `toolfuncs TOOL --help` when runtime command inspection is needed.
179
+
180
+ ## Development
181
+
182
+ ```console
183
+ uv sync
184
+ uv run pytest
185
+ uv run ruff format --check .
186
+ uv run ruff check .
187
+ uv run basedpyright
188
+ ```
189
+
190
+ The detailed v1 guarantees and non-goals are recorded in [the contract](docs/contract.md).
191
+
192
+ ## Releasing
193
+
194
+ The GitHub Release is the release control point. Set `[project].version`, merge and push that commit, then publish a GitHub Release whose tag is `v<version>`. The release workflow verifies that the tag and package version match, builds the wheel and source distribution, and publishes them to PyPI through the `pypi` environment and PyPI Trusted Publishing. It uses no long-lived PyPI token or repository secret.
@@ -0,0 +1,70 @@
1
+ # toolfuncs v1 contract
2
+
3
+ ## Purpose
4
+
5
+ `toolfuncs` makes a Python function interface callable through two projections without defining two domain APIs. Python import and CLI invocation differ only at their unavoidable transport boundaries: CLI strings become annotated Python values, returned objects become strict JSON, and exceptions become nonzero process failures.
6
+
7
+ This is intentionally a convention and implementation, not a transport protocol or registry specification. It occupies a lower level than MCP: a tool is executable Python in the caller's interpreter, and composition is an ordinary function call.
8
+
9
+ ## Authoring contract
10
+
11
+ A tool is one Python module. Its authoritative operations are typed functions registered directly on a module-level `app`.
12
+
13
+ - Define `app = toolfuncs.App()` and decorate each CLI-callable operation with `@app.command`.
14
+ - `toolfuncs.App` is a Cyclopts App whose default result action serializes through `pydantic-core` as strict JSON. An explicit `result_action=` replaces that default unchanged.
15
+ - `toolfuncs` re-exports the Cyclopts and `pydantic-core` authoring values needed by tools, so the tool does not import those libraries separately.
16
+ - Only registered functions form the CLI surface. `__all__` has its ordinary Python meaning and does not register commands.
17
+ - Function signatures, annotations, defaults, return values, docstrings, side effects, and exceptions define the operation.
18
+ - `[tool.toolfuncs].description` contains the required one-line static tool description. Custom metadata dunders are not part of the contract.
19
+ - Importing a tool must not execute an operation, print, configure process-wide state, or exit.
20
+ - `app()` belongs behind `if __name__ == "__main__":` when the source should also be directly executable.
21
+
22
+ CLI command and option spelling follows Cyclopts' deterministic Python-to-kebab-case projection. Successful CLI operations emit one strict JSON value. No fallback converts unknown values to strings, and non-finite floats are rejected.
23
+
24
+ ## Scope contract
25
+
26
+ The effective tool set is the merge of the nearest ancestor `.agents/tools` directory over `~/.agents/tools`. Only valid immediate PEP 723 files and packaged project directories participate. Project definitions shadow user definitions by logical name. A logical name must be a valid non-keyword Python identifier.
27
+
28
+ The initial working directory and home directory are frozen at the first `toolfuncs` import. Explicit `cwd=` and `home=` parameters exist on discovery APIs for deterministic inspection and testing; they do not mutate the frozen default context.
29
+
30
+ ## Discoverable source contract
31
+
32
+ Exactly two source shapes define scoped tools:
33
+
34
+ 1. `<name>.py` contains one PEP 723 `script` block with `[tool.toolfuncs].description`;
35
+ 2. `<name>/` contains `pyproject.toml` and `src/<name>/__init__.py`.
36
+
37
+ For a packaged tool, `[project].name` must normalize to `<name>`, `[build-system]` must explicitly declare `requires` and `build-backend`, and `[tool.toolfuncs].description` must be present. The directory name, scoped name, and Python import name are exactly `<name>`. Tool loading passes that known name to the generic importer; there is no tool-level import-name inference or configuration.
38
+
39
+ Any immediate `.py` file or directory in a tool root that does not satisfy one of these contracts is invalid. In particular, direct package directories, flat projects, `src/<name>.py`, namespace roots, legacy `setup.py` projects, wheels, source archives, and remote sources are not discoverable tool forms.
40
+
41
+ `toolfuncs list` and `toolfuncs describe` parse static metadata without importing source, resolving runtime dependencies, or invoking a build backend. Exact runtime commands remain defined by the module's `app` and are inspected through `toolfuncs <name> --help`.
42
+
43
+ ## Generic import utility
44
+
45
+ `import_path()` remains broader than the tool convention. It accepts direct Python files and packages, installable `pyproject.toml` and legacy `setup.py` projects, wheels, source archives, and corresponding fsspec URLs. Direct packages read dependencies only from adjacent `[project]` metadata. Projects and artifacts are reduced to one wheel, whose installed Python roots are inspected for import selection.
46
+
47
+ An arbitrary caller may pass explicit `import_name=` when the source does not have one unambiguous import. `import_path()` does not read `[tool.toolfuncs].import-name` or any other project configuration for this purpose.
48
+
49
+ ## Identity contract
50
+
51
+ One interpreter maintains a bijection between canonical sources and registered import names. Repeating a pair returns the same module. Attempting to bind either side to a different counterpart raises `ImportConflictError`.
52
+
53
+ Local source identity is its expanded absolute path. Remote source identity retains protocol, host, path, query, and fragment but removes URL user information. User-facing remote diagnostics additionally remove query strings. Storage credentials are neither identity nor metadata.
54
+
55
+ An already loaded `sys.modules` entry that toolfuncs does not own is a conflict for a direct or installed import. V1 does not guess whether that module came from the same distribution.
56
+
57
+ ## Installation contract
58
+
59
+ PEP 723 and direct-package dependencies are installed with the running interpreter's `pip`. Packaged tools, generic projects, and source archives are built to a wheel with `pip wheel --no-deps`; the selected wheel is then installed with pip so its declared runtime dependencies are resolved normally. Imports are invalidated after environment changes.
60
+
61
+ Resolution, materialization, dependency installation, module registration, and execution are serialized by one reentrant process lock. Failed direct execution removes its module and any synthetic parent packages that remain unused.
62
+
63
+ ## Explicit v1 non-goals
64
+
65
+ - No subprocess, RPC, or object-proxy isolation.
66
+ - No dependency solver spanning several tools before import.
67
+ - No alternate discoverable project layouts or tool-level import-name escape hatch.
68
+ - No portable promise for `import toolfuncs.NAME`; use `from toolfuncs import NAME`.
69
+ - No automatic shell-profile edits or daemon. `sync` creates additive shims only.
70
+ - No `TOOL.md` or command manifest. Static TOML describes the tool; Python definitions remain authoritative for runtime operations.
@@ -0,0 +1,51 @@
1
+ [project]
2
+ name = "toolfuncs"
3
+ version = "0.1.0"
4
+ description = "Function-first Python tools with matching import and command-line interfaces"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Nima Shoghi", email = "nima@boltz.bio" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "cyclopts>=4.23.2,<5",
12
+ "fsspec[github,http]>=2025.3.0",
13
+ "packaging>=24",
14
+ "pip>=25",
15
+ "pydantic-core>=2.20,<3",
16
+ ]
17
+
18
+ [project.scripts]
19
+ toolfuncs = "toolfuncs.__main__:main"
20
+
21
+ [project.urls]
22
+ Repository = "https://github.com/nimashoghi/toolfuncs"
23
+ Issues = "https://github.com/nimashoghi/toolfuncs/issues"
24
+
25
+ [build-system]
26
+ requires = ["hatchling"]
27
+ build-backend = "hatchling.build"
28
+
29
+ [dependency-groups]
30
+ dev = [
31
+ "basedpyright>=1.31",
32
+ "build>=1.2",
33
+ "pytest>=8",
34
+ "pytest-cov>=6",
35
+ "ruff>=0.12",
36
+ ]
37
+
38
+ [tool.ruff]
39
+ line-length = 100
40
+ target-version = "py311"
41
+
42
+ [tool.ruff.lint]
43
+ select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]
44
+
45
+ [tool.pytest.ini_options]
46
+ addopts = "-ra"
47
+ testpaths = ["tests"]
48
+
49
+ [tool.basedpyright]
50
+ pythonVersion = "3.11"
51
+ typeCheckingMode = "standard"