pytruenas 0.0.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.
- pytruenas-0.0.0/.gitignore +43 -0
- pytruenas-0.0.0/CHANGELOG.md +104 -0
- pytruenas-0.0.0/CONTRIBUTING.md +87 -0
- pytruenas-0.0.0/LICENSE +21 -0
- pytruenas-0.0.0/PKG-INFO +182 -0
- pytruenas-0.0.0/README.md +106 -0
- pytruenas-0.0.0/RELEASENOTES.md +73 -0
- pytruenas-0.0.0/benchmarks/README.md +58 -0
- pytruenas-0.0.0/benchmarks/results/pytruenas-0.1.0-py314.json +46 -0
- pytruenas-0.0.0/benchmarks/results/pytruenas-0.1.0-py39.json +46 -0
- pytruenas-0.0.0/benchmarks/run.py +129 -0
- pytruenas-0.0.0/docs/api/auth.md +3 -0
- pytruenas-0.0.0/docs/api/client.md +3 -0
- pytruenas-0.0.0/docs/api/fs.md +3 -0
- pytruenas-0.0.0/docs/api/jsonrpc.md +11 -0
- pytruenas-0.0.0/docs/api/namespace.md +5 -0
- pytruenas-0.0.0/docs/api/reference.md +12 -0
- pytruenas-0.0.0/docs/changelog.md +3 -0
- pytruenas-0.0.0/docs/guide/api.md +57 -0
- pytruenas-0.0.0/docs/guide/cli.md +70 -0
- pytruenas-0.0.0/docs/guide/commands.md +30 -0
- pytruenas-0.0.0/docs/guide/connecting.md +43 -0
- pytruenas-0.0.0/docs/guide/filesystem.md +33 -0
- pytruenas-0.0.0/docs/guide/typings.md +28 -0
- pytruenas-0.0.0/docs/index.md +58 -0
- pytruenas-0.0.0/mkdocs.yml +63 -0
- pytruenas-0.0.0/pyproject.toml +85 -0
- pytruenas-0.0.0/src/pytruenas/AGENTS.md +319 -0
- pytruenas-0.0.0/src/pytruenas/__init__.py +13 -0
- pytruenas-0.0.0/src/pytruenas/__main__.py +7 -0
- pytruenas-0.0.0/src/pytruenas/_conn.py +21 -0
- pytruenas-0.0.0/src/pytruenas/auth.py +126 -0
- pytruenas-0.0.0/src/pytruenas/client.py +471 -0
- pytruenas-0.0.0/src/pytruenas/cmd/call.py +60 -0
- pytruenas-0.0.0/src/pytruenas/cmd/dump_api.py +20 -0
- pytruenas-0.0.0/src/pytruenas/cmd/generate_typings.py +74 -0
- pytruenas-0.0.0/src/pytruenas/cmd/query.py +47 -0
- pytruenas-0.0.0/src/pytruenas/codegen/__init__.py +502 -0
- pytruenas-0.0.0/src/pytruenas/codegen/jinja.py +18 -0
- pytruenas-0.0.0/src/pytruenas/codegen/namespace.pyi.j2 +37 -0
- pytruenas-0.0.0/src/pytruenas/fs/__init__.py +65 -0
- pytruenas-0.0.0/src/pytruenas/fs/tnasws.py +201 -0
- pytruenas-0.0.0/src/pytruenas/fs/truenas.py +211 -0
- pytruenas-0.0.0/src/pytruenas/jsonrpc.py +414 -0
- pytruenas-0.0.0/src/pytruenas/main.py +173 -0
- pytruenas-0.0.0/src/pytruenas/models/__init__.py +0 -0
- pytruenas-0.0.0/src/pytruenas/models/apidump.py +39 -0
- pytruenas-0.0.0/src/pytruenas/models/jsonschema.py +237 -0
- pytruenas-0.0.0/src/pytruenas/namespace.py +331 -0
- pytruenas-0.0.0/src/pytruenas/ops/__init__.py +32 -0
- pytruenas-0.0.0/src/pytruenas/ops/host.py +148 -0
- pytruenas-0.0.0/src/pytruenas/ops/main.py +71 -0
- pytruenas-0.0.0/src/pytruenas/ops/midclt.py +229 -0
- pytruenas-0.0.0/src/pytruenas/ops/template.py +124 -0
- pytruenas-0.0.0/src/pytruenas/py.typed +0 -0
- pytruenas-0.0.0/src/pytruenas/utils/__init__.py +0 -0
- pytruenas-0.0.0/src/pytruenas/utils/async_.py +90 -0
- pytruenas-0.0.0/src/pytruenas/utils/cmd.py +147 -0
- pytruenas-0.0.0/src/pytruenas/utils/io.py +21 -0
- pytruenas-0.0.0/src/pytruenas/utils/query.py +121 -0
- pytruenas-0.0.0/src/pytruenas/utils/target.py +87 -0
- pytruenas-0.0.0/tests/fixtures/api_min.json +132 -0
- pytruenas-0.0.0/tests/test_async.py +64 -0
- pytruenas-0.0.0/tests/test_auth.py +70 -0
- pytruenas-0.0.0/tests/test_cli_call.py +62 -0
- pytruenas-0.0.0/tests/test_cli_targets.py +64 -0
- pytruenas-0.0.0/tests/test_client.py +89 -0
- pytruenas-0.0.0/tests/test_client_more.py +199 -0
- pytruenas-0.0.0/tests/test_cmd_config.py +38 -0
- pytruenas-0.0.0/tests/test_codegen.py +101 -0
- pytruenas-0.0.0/tests/test_fs_local.py +48 -0
- pytruenas-0.0.0/tests/test_fs_tnas.py +116 -0
- pytruenas-0.0.0/tests/test_jsonrpc.py +102 -0
- pytruenas-0.0.0/tests/test_jsonrpc_client.py +192 -0
- pytruenas-0.0.0/tests/test_main.py +112 -0
- pytruenas-0.0.0/tests/test_namespace.py +174 -0
- pytruenas-0.0.0/tests/test_namespace_cache.py +50 -0
- pytruenas-0.0.0/tests/test_namespace_retry.py +85 -0
- pytruenas-0.0.0/tests/test_ops_midclt.py +27 -0
- pytruenas-0.0.0/tests/test_ops_template.py +83 -0
- pytruenas-0.0.0/tests/test_package.py +17 -0
- pytruenas-0.0.0/tests/test_query.py +37 -0
- pytruenas-0.0.0/tests/test_target.py +82 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Agent configs / private notes — never tracked in this repo
|
|
2
|
+
.agents
|
|
3
|
+
*.local.md
|
|
4
|
+
CLAUDE*
|
|
5
|
+
.claude/
|
|
6
|
+
|
|
7
|
+
# Virtualenvs
|
|
8
|
+
.venv*/
|
|
9
|
+
venv/
|
|
10
|
+
env/
|
|
11
|
+
.env/
|
|
12
|
+
|
|
13
|
+
# Byte-compiled / optimized
|
|
14
|
+
__pycache__/
|
|
15
|
+
*.py[cod]
|
|
16
|
+
*$py.class
|
|
17
|
+
|
|
18
|
+
# Distribution / packaging
|
|
19
|
+
build/
|
|
20
|
+
dist/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
*.egg
|
|
23
|
+
.eggs/
|
|
24
|
+
|
|
25
|
+
# Test / coverage
|
|
26
|
+
.pytest_cache/
|
|
27
|
+
.coverage
|
|
28
|
+
htmlcov/
|
|
29
|
+
.tox/
|
|
30
|
+
|
|
31
|
+
# Docs site build
|
|
32
|
+
/site
|
|
33
|
+
docs/_build/
|
|
34
|
+
|
|
35
|
+
# Editors / OS
|
|
36
|
+
.vscode/
|
|
37
|
+
.idea/
|
|
38
|
+
*.swp
|
|
39
|
+
.DS_Store
|
|
40
|
+
|
|
41
|
+
# Local pytruenas config / generated typings
|
|
42
|
+
pytruenas.yaml
|
|
43
|
+
/typings/
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.0.0] - 2026-07-22
|
|
10
|
+
|
|
11
|
+
Initial release.
|
|
12
|
+
|
|
13
|
+
Earlier version numbers appear in this project's git history but were never
|
|
14
|
+
tagged or published, so there is no upgrade path to describe -- everything
|
|
15
|
+
below is simply what the package contains.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- **`ws://` and `wss://` URLs no longer parse as port 0.** No system services
|
|
19
|
+
database has an entry for the websocket schemes, so `getservbyname("wss")`
|
|
20
|
+
failed -- and those are the schemes this client uses most. Port resolution now
|
|
21
|
+
goes through `netimps`, whose scheme table is consulted before the system
|
|
22
|
+
database.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **`pytruenas call <method>` command.** Invoke any middleware method by its
|
|
26
|
+
dotted name (`system.info`, `core.ping`, `pool.dataset.details`) — not just
|
|
27
|
+
the queryable `<namespace>.query` methods `query` covers. Parameters are JSON
|
|
28
|
+
values via `-p/--param` (repeatable).
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- **CLI targets are now trailing positional arguments, not `-t/--target`.**
|
|
32
|
+
A command's own positionals come first, then the target host(s):
|
|
33
|
+
`pytruenas query user nas1 nas2`, `pytruenas dump-api nas1,nas2`. Comma lists
|
|
34
|
+
and `[A-Z]`/`[0-9]` range patterns still expand; no target means `localhost`.
|
|
35
|
+
The `-t`/`--target` flag has been removed.
|
|
36
|
+
|
|
37
|
+
- Dependency floors raised to the validated versions: `duho>=0.3.2` (CLI parser
|
|
38
|
+
fixes — a global option before a subcommand is no longer shadowed; a literal
|
|
39
|
+
`%` in a `Cmd` docstring no longer breaks parser build) and the `ssh` extra's
|
|
40
|
+
`pathlib_next[sftp-async]>=0.8.3` (SFTP default concurrency raised 8→16).
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
- **API calls no longer silently return `None` on a dropped connection.** The
|
|
44
|
+
namespace call retry loop fell through and returned `None` after a single
|
|
45
|
+
`ECONNABORTED` — which `_get` read as "record missing", turning an `_upsert`
|
|
46
|
+
into a spurious create (possible duplicate rows). It now retries then raises,
|
|
47
|
+
and never returns `None` on a connection error.
|
|
48
|
+
- **Long-running jobs no longer spuriously time out.** `core.job_wait` (waited
|
|
49
|
+
on after uploads/downloads and mutating `_upsert`/`_update` calls) is now
|
|
50
|
+
issued with no client-side timeout, so a job lasting longer than the 60s
|
|
51
|
+
default no longer raises `CallTimeout` while it is still running server-side.
|
|
52
|
+
`Client.call(timeout=None)` now means "wait indefinitely".
|
|
53
|
+
- `client.run()` with a `str` `input` together with a text `encoding`/`errors`
|
|
54
|
+
no longer crashes. It used to pre-encode the string to bytes *and* hand the
|
|
55
|
+
encoding to `subprocess.run`, which then tried to `.encode()` the already-bytes
|
|
56
|
+
input (`AttributeError`). Now text mode keeps `str` input as-is (and decodes
|
|
57
|
+
`bytes` input), binary mode encodes. Found by live testing on TrueNAS 26.0.
|
|
58
|
+
|
|
59
|
+
- `ops.template.TemplateTarget.apply_template` no longer crashes on a plain
|
|
60
|
+
string template (`issubclass()` was called on a non-type); a `str` is now
|
|
61
|
+
treated as literal template content and a path-like is read as file content.
|
|
62
|
+
- `namespace.ioerror` only maps a middleware error to `OSError` when the
|
|
63
|
+
bracketed prefix names a real POSIX errno; previously an unrecognised prefix
|
|
64
|
+
produced `IOError(None, msg)`, discarding the original exception type.
|
|
65
|
+
|
|
66
|
+
### Internal
|
|
67
|
+
- `jsonrpc.Client.call` narrows the compatibility kwargs it ignores and logs any
|
|
68
|
+
other unexpected keyword at debug level instead of silently swallowing it;
|
|
69
|
+
`_ioerror` is no longer forwarded into the upload/download paths.
|
|
70
|
+
- `Namespace` child lookups use a per-instance dict instead of `functools.cache`
|
|
71
|
+
on the methods, so namespaces are garbage-collected with their client instead
|
|
72
|
+
of being pinned for the process lifetime (relevant to long-lived embeddings).
|
|
73
|
+
- The `pytruenas.ops` subpackage (systemd/midclt host-config helpers) is
|
|
74
|
+
**experimental** and exercised only by unit tests, not against a live host.
|
|
75
|
+
|
|
76
|
+
### Added
|
|
77
|
+
- Packaged as `pytruenas` (src layout, hatchling, `pytruenas` console script,
|
|
78
|
+
`py.typed`). Python 3.9+.
|
|
79
|
+
- Lean in-house JSON-RPC 2.0 client (`pytruenas.jsonrpc`) speaking the middleware
|
|
80
|
+
protocol over `wss://`/`ws://` and the local `ws+unix://` socket, with
|
|
81
|
+
extended-JSON (datetime/date/time/set/IP) round-tripping and
|
|
82
|
+
`ClientException`/`ValidationErrors` mapping. Verified against a live host.
|
|
83
|
+
- Attribute-style API namespace (`client.api.<namespace>.<method>(...)`) with
|
|
84
|
+
`_get`/`_query`/`_create`/`_update`/`_upsert` convenience helpers.
|
|
85
|
+
- Filesystem paths on `pathlib_next`: `client.path()` returns a `LocalPath`
|
|
86
|
+
(local) or `TruenasPath` (remote — SFTP-preferred via pathlib_next's `SftpPath`,
|
|
87
|
+
falling back to the middleware `filesystem.*` websocket API for
|
|
88
|
+
delete/rename/symlink).
|
|
89
|
+
- Typings generator (`generate-typings`): produces `.pyi` stubs for the whole
|
|
90
|
+
API, validated to parse across every version of a real v26 dump (780 methods).
|
|
91
|
+
- CLI (`dump-api`, `query`, `generate-typings`) on `duho` with multi-target
|
|
92
|
+
fan-out (`-t/--target`, `--parallel`) and optional YAML config.
|
|
93
|
+
- Optional extras: `ssh`, `config`, `codegen`, `host`.
|
|
94
|
+
- Test suite green on Python 3.9 and 3.13/3.14.
|
|
95
|
+
|
|
96
|
+
### Notes
|
|
97
|
+
- Runtime CLI/logging/qualname/text come from `duho` (>=0.3.0); path types from
|
|
98
|
+
`pathlib_next` (>=0.8.2). Both are on PyPI.
|
|
99
|
+
- Remote shell command execution (`client.run` over SSH) uses `asyncssh` (the
|
|
100
|
+
`ssh` extra); the middleware API has no command-exec method. SFTP is handled by
|
|
101
|
+
`pathlib_next`.
|
|
102
|
+
|
|
103
|
+
[Unreleased]: https://github.com/jose-pr/pytruenas/compare/v0.0.0...HEAD
|
|
104
|
+
[0.0.0]: https://github.com/jose-pr/pytruenas/releases/tag/v0.0.0
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Contributing to pytruenas
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in contributing! Here's how to get started.
|
|
4
|
+
|
|
5
|
+
## Development Setup
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
git clone https://github.com/jose-pr/pytruenas.git
|
|
9
|
+
cd pytruenas
|
|
10
|
+
|
|
11
|
+
# Create a virtual environment (project targets Python 3.9+)
|
|
12
|
+
python -m venv .venv39
|
|
13
|
+
.venv39/Scripts/activate # Windows; on Unix: source .venv39/bin/activate
|
|
14
|
+
|
|
15
|
+
# Install in development mode with the test dependencies
|
|
16
|
+
pip install -e ".[dev]"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The `ssh`, `config`, `codegen`, and `host` extras enable optional features; add
|
|
20
|
+
the ones whose tests you want to run (e.g. `-e ".[dev,ssh]"`).
|
|
21
|
+
|
|
22
|
+
## Running Tests
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pytest -q
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
With coverage:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pytest -q --cov=pytruenas --cov-report=term-missing
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Some `client.run()` tests require a POSIX shell and skip on Windows; they run on
|
|
35
|
+
Linux/macOS (and on a real NAS). "0 failures" with those skipped is expected on
|
|
36
|
+
Windows — run `pytest -rs` to see skip reasons.
|
|
37
|
+
|
|
38
|
+
## Running Benchmarks
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
python benchmarks/run.py # print summary
|
|
42
|
+
python benchmarks/run.py --save # write benchmarks/results/<name>.json
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
See [`benchmarks/README.md`](benchmarks/README.md) for the schema and what the
|
|
46
|
+
numbers mean.
|
|
47
|
+
|
|
48
|
+
## Docs
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install -e ".[docs]"
|
|
52
|
+
mkdocs serve # live preview at http://127.0.0.1:8000
|
|
53
|
+
mkdocs build --strict # what CI gates on
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Code Style
|
|
57
|
+
|
|
58
|
+
- Follow PEP 8; use type hints.
|
|
59
|
+
- `from __future__ import annotations` in any module using `X | Y` unions at
|
|
60
|
+
runtime — the project supports Python 3.9.
|
|
61
|
+
- Keep the vendored-free, minimal-dependency posture: guard optional imports and
|
|
62
|
+
degrade gracefully rather than hard-crashing on a missing extra.
|
|
63
|
+
|
|
64
|
+
## Commit Guidelines
|
|
65
|
+
|
|
66
|
+
Format: `type: description`
|
|
67
|
+
|
|
68
|
+
- `feat:` new feature
|
|
69
|
+
- `fix:` bug fix
|
|
70
|
+
- `docs:` documentation
|
|
71
|
+
- `test:` tests
|
|
72
|
+
- `chore:` build, CI, deps, or tooling
|
|
73
|
+
|
|
74
|
+
Keep commits logical and separate (a feature and its tests together; docs/config
|
|
75
|
+
apart from behavior changes).
|
|
76
|
+
|
|
77
|
+
## Pull Request Process
|
|
78
|
+
|
|
79
|
+
1. Branch: `git checkout -b feat/my-feature`
|
|
80
|
+
2. Make changes and add tests.
|
|
81
|
+
3. `pytest -q` green; `mkdocs build --strict` clean if docs changed.
|
|
82
|
+
4. Commit with a clear message and open a PR.
|
|
83
|
+
|
|
84
|
+
## Reporting Issues
|
|
85
|
+
|
|
86
|
+
Include your Python version, pytruenas version, the TrueNAS version, a minimal
|
|
87
|
+
reproducer, and expected vs. actual behavior.
|
pytruenas-0.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jose A.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pytruenas-0.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytruenas
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: A typed, Pythonic client and CLI for the TrueNAS middleware API
|
|
5
|
+
Project-URL: Homepage, https://github.com/jose-pr/pytruenas/
|
|
6
|
+
Project-URL: Issues, https://github.com/jose-pr/pytruenas/issues
|
|
7
|
+
Project-URL: Repository, https://github.com/jose-pr/pytruenas
|
|
8
|
+
Author: Jose A.
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Jose A.
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: cli,jsonrpc,middleware,sysadmin,truenas,websocket
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: Intended Audience :: System Administrators
|
|
36
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
37
|
+
Classifier: Operating System :: OS Independent
|
|
38
|
+
Classifier: Programming Language :: Python :: 3
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
41
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
42
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
43
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
44
|
+
Classifier: Topic :: System :: Filesystems
|
|
45
|
+
Classifier: Topic :: System :: Systems Administration
|
|
46
|
+
Classifier: Typing :: Typed
|
|
47
|
+
Requires-Python: >=3.9
|
|
48
|
+
Requires-Dist: duho>=0.3.2
|
|
49
|
+
Requires-Dist: netimps>=0.0.1
|
|
50
|
+
Requires-Dist: pathlib-next[uri]>=0.8.2
|
|
51
|
+
Requires-Dist: requests
|
|
52
|
+
Requires-Dist: websocket-client
|
|
53
|
+
Provides-Extra: codegen
|
|
54
|
+
Requires-Dist: jinja2; extra == 'codegen'
|
|
55
|
+
Provides-Extra: config
|
|
56
|
+
Requires-Dist: pyyaml; extra == 'config'
|
|
57
|
+
Provides-Extra: dev
|
|
58
|
+
Requires-Dist: asyncssh; extra == 'dev'
|
|
59
|
+
Requires-Dist: build; extra == 'dev'
|
|
60
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
61
|
+
Requires-Dist: jinja2; extra == 'dev'
|
|
62
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
63
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
64
|
+
Requires-Dist: pyyaml; extra == 'dev'
|
|
65
|
+
Requires-Dist: twine; extra == 'dev'
|
|
66
|
+
Provides-Extra: docs
|
|
67
|
+
Requires-Dist: mkdocs; extra == 'docs'
|
|
68
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
69
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
70
|
+
Provides-Extra: host
|
|
71
|
+
Requires-Dist: ifaddr; extra == 'host'
|
|
72
|
+
Provides-Extra: ssh
|
|
73
|
+
Requires-Dist: asyncssh; extra == 'ssh'
|
|
74
|
+
Requires-Dist: pathlib-next[sftp-async]>=0.8.3; extra == 'ssh'
|
|
75
|
+
Description-Content-Type: text/markdown
|
|
76
|
+
|
|
77
|
+
# pytruenas
|
|
78
|
+
|
|
79
|
+
[](https://pypi.org/project/pytruenas/)
|
|
80
|
+
[](https://pypi.org/project/pytruenas/)
|
|
81
|
+
[](https://jose-pr.github.io/pytruenas/)
|
|
82
|
+
[](https://github.com/jose-pr/pytruenas/blob/main/LICENSE)
|
|
83
|
+
|
|
84
|
+
A typed, Pythonic client and CLI for the [TrueNAS](https://www.truenas.com/)
|
|
85
|
+
middleware API.
|
|
86
|
+
|
|
87
|
+
`pytruenas` speaks the middleware's JSON-RPC 2.0 websocket protocol directly —
|
|
88
|
+
over `wss://`/`ws://` to a remote host, or over the local unix socket
|
|
89
|
+
(`ws+unix://`) when running on the NAS itself. It exposes the whole API surface
|
|
90
|
+
through an attribute-style namespace, adds convenience helpers for the common
|
|
91
|
+
create/update/upsert patterns, a remote-filesystem abstraction, an optional
|
|
92
|
+
typings generator, and a small CLI for scripting host configuration.
|
|
93
|
+
|
|
94
|
+
## Install
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
pip install pytruenas # once published to PyPI
|
|
98
|
+
# or, from a checkout:
|
|
99
|
+
pip install .
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Optional extras:
|
|
103
|
+
|
|
104
|
+
| Extra | Enables |
|
|
105
|
+
| ----- | ------- |
|
|
106
|
+
| `pytruenas[ssh]` | Remote shell + SFTP filesystem backend (`asyncssh`) |
|
|
107
|
+
| `pytruenas[config]` | YAML config/targets file for the CLI (`pyyaml`) |
|
|
108
|
+
| `pytruenas[codegen]` | `generate-typings` command (`jinja2`) |
|
|
109
|
+
| `pytruenas[host]` | Local network-adapter / packaging helpers (`ifaddr`) |
|
|
110
|
+
|
|
111
|
+
## Quickstart
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from pytruenas import TrueNASClient
|
|
115
|
+
|
|
116
|
+
# Remote host (api key, or "user:password", or a token)
|
|
117
|
+
client = TrueNASClient("nas.example.com", "1-<64-char-api-key>", sslverify=False)
|
|
118
|
+
|
|
119
|
+
# Attribute-style access to any API namespace/method:
|
|
120
|
+
for user in client.api.user.query():
|
|
121
|
+
print(user["username"])
|
|
122
|
+
|
|
123
|
+
# Convenience helpers for common DB patterns:
|
|
124
|
+
client.api.user._upsert("username", username="svc", full_name="Service", group_create=True)
|
|
125
|
+
|
|
126
|
+
# Running on the NAS itself talks to the local unix socket, no auth:
|
|
127
|
+
local = TrueNASClient() # ws+unix:///var/run/middleware/middlewared.sock
|
|
128
|
+
print(local.api.system.info())
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Credentials
|
|
132
|
+
|
|
133
|
+
`TrueNASClient(target, creds)` accepts, for `creds`:
|
|
134
|
+
|
|
135
|
+
- an **API key** string `"<id>-<64 chars>"`,
|
|
136
|
+
- `"user:password"` (optionally `"user:password\n<otp>"`),
|
|
137
|
+
- a **token** string,
|
|
138
|
+
- a `(user, password)` tuple,
|
|
139
|
+
- `None` / omitted → local socket auth.
|
|
140
|
+
|
|
141
|
+
`Credentials.from_env()` reads `TN_CREDS`.
|
|
142
|
+
|
|
143
|
+
## CLI
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
pytruenas --help
|
|
147
|
+
pytruenas query user -f username=root nas.example.com
|
|
148
|
+
pytruenas call system.info nas.example.com # any method by dotted name
|
|
149
|
+
pytruenas dump-api nas.example.com > api.json
|
|
150
|
+
pytruenas generate-typings --path typings --api-version v26.0.0 nas.example.com
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
The target host(s) are the **trailing positional arguments** — a command's own
|
|
154
|
+
positionals (like `query`'s namespace) come first, then the hosts. Each target
|
|
155
|
+
may be comma-separated and supports `[A-Z]`/`[0-9]` range expansion (e.g.
|
|
156
|
+
`'nas[1-3].example.com'`); with no target the command runs against `localhost`.
|
|
157
|
+
`--parallel N` runs several targets concurrently. Filter `query` with
|
|
158
|
+
`-f/--filter KEY=VALUE` (repeatable).
|
|
159
|
+
|
|
160
|
+
## Typings generator
|
|
161
|
+
|
|
162
|
+
`generate-typings` turns a host's API definition into a package of `.pyi` stubs
|
|
163
|
+
so editors and type checkers understand `client.api.<namespace>.<method>(...)`.
|
|
164
|
+
It is validated against the full real API (every version in a live dump).
|
|
165
|
+
|
|
166
|
+
```sh
|
|
167
|
+
pytruenas generate-typings --path truenasapi_typings/current nas.example.com
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Development
|
|
171
|
+
|
|
172
|
+
```sh
|
|
173
|
+
py -3.14 -m venv .venv
|
|
174
|
+
.venv/Scripts/python -m pip install -e .[dev]
|
|
175
|
+
.venv/Scripts/python -m pytest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Supports Python 3.9+.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# pytruenas
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pytruenas/)
|
|
4
|
+
[](https://pypi.org/project/pytruenas/)
|
|
5
|
+
[](https://jose-pr.github.io/pytruenas/)
|
|
6
|
+
[](https://github.com/jose-pr/pytruenas/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
A typed, Pythonic client and CLI for the [TrueNAS](https://www.truenas.com/)
|
|
9
|
+
middleware API.
|
|
10
|
+
|
|
11
|
+
`pytruenas` speaks the middleware's JSON-RPC 2.0 websocket protocol directly —
|
|
12
|
+
over `wss://`/`ws://` to a remote host, or over the local unix socket
|
|
13
|
+
(`ws+unix://`) when running on the NAS itself. It exposes the whole API surface
|
|
14
|
+
through an attribute-style namespace, adds convenience helpers for the common
|
|
15
|
+
create/update/upsert patterns, a remote-filesystem abstraction, an optional
|
|
16
|
+
typings generator, and a small CLI for scripting host configuration.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pip install pytruenas # once published to PyPI
|
|
22
|
+
# or, from a checkout:
|
|
23
|
+
pip install .
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Optional extras:
|
|
27
|
+
|
|
28
|
+
| Extra | Enables |
|
|
29
|
+
| ----- | ------- |
|
|
30
|
+
| `pytruenas[ssh]` | Remote shell + SFTP filesystem backend (`asyncssh`) |
|
|
31
|
+
| `pytruenas[config]` | YAML config/targets file for the CLI (`pyyaml`) |
|
|
32
|
+
| `pytruenas[codegen]` | `generate-typings` command (`jinja2`) |
|
|
33
|
+
| `pytruenas[host]` | Local network-adapter / packaging helpers (`ifaddr`) |
|
|
34
|
+
|
|
35
|
+
## Quickstart
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from pytruenas import TrueNASClient
|
|
39
|
+
|
|
40
|
+
# Remote host (api key, or "user:password", or a token)
|
|
41
|
+
client = TrueNASClient("nas.example.com", "1-<64-char-api-key>", sslverify=False)
|
|
42
|
+
|
|
43
|
+
# Attribute-style access to any API namespace/method:
|
|
44
|
+
for user in client.api.user.query():
|
|
45
|
+
print(user["username"])
|
|
46
|
+
|
|
47
|
+
# Convenience helpers for common DB patterns:
|
|
48
|
+
client.api.user._upsert("username", username="svc", full_name="Service", group_create=True)
|
|
49
|
+
|
|
50
|
+
# Running on the NAS itself talks to the local unix socket, no auth:
|
|
51
|
+
local = TrueNASClient() # ws+unix:///var/run/middleware/middlewared.sock
|
|
52
|
+
print(local.api.system.info())
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Credentials
|
|
56
|
+
|
|
57
|
+
`TrueNASClient(target, creds)` accepts, for `creds`:
|
|
58
|
+
|
|
59
|
+
- an **API key** string `"<id>-<64 chars>"`,
|
|
60
|
+
- `"user:password"` (optionally `"user:password\n<otp>"`),
|
|
61
|
+
- a **token** string,
|
|
62
|
+
- a `(user, password)` tuple,
|
|
63
|
+
- `None` / omitted → local socket auth.
|
|
64
|
+
|
|
65
|
+
`Credentials.from_env()` reads `TN_CREDS`.
|
|
66
|
+
|
|
67
|
+
## CLI
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
pytruenas --help
|
|
71
|
+
pytruenas query user -f username=root nas.example.com
|
|
72
|
+
pytruenas call system.info nas.example.com # any method by dotted name
|
|
73
|
+
pytruenas dump-api nas.example.com > api.json
|
|
74
|
+
pytruenas generate-typings --path typings --api-version v26.0.0 nas.example.com
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The target host(s) are the **trailing positional arguments** — a command's own
|
|
78
|
+
positionals (like `query`'s namespace) come first, then the hosts. Each target
|
|
79
|
+
may be comma-separated and supports `[A-Z]`/`[0-9]` range expansion (e.g.
|
|
80
|
+
`'nas[1-3].example.com'`); with no target the command runs against `localhost`.
|
|
81
|
+
`--parallel N` runs several targets concurrently. Filter `query` with
|
|
82
|
+
`-f/--filter KEY=VALUE` (repeatable).
|
|
83
|
+
|
|
84
|
+
## Typings generator
|
|
85
|
+
|
|
86
|
+
`generate-typings` turns a host's API definition into a package of `.pyi` stubs
|
|
87
|
+
so editors and type checkers understand `client.api.<namespace>.<method>(...)`.
|
|
88
|
+
It is validated against the full real API (every version in a live dump).
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
pytruenas generate-typings --path truenasapi_typings/current nas.example.com
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
py -3.14 -m venv .venv
|
|
98
|
+
.venv/Scripts/python -m pip install -e .[dev]
|
|
99
|
+
.venv/Scripts/python -m pytest
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Supports Python 3.9+.
|
|
103
|
+
|
|
104
|
+
## License
|
|
105
|
+
|
|
106
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Release Notes
|
|
2
|
+
|
|
3
|
+
Detailed notes per release: the narrative, the performance story, and the
|
|
4
|
+
validation evidence behind each version. `CHANGELOG.md` stays terse and
|
|
5
|
+
user-facing; this file is the durable record.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## [Unreleased]
|
|
10
|
+
|
|
11
|
+
### What changed
|
|
12
|
+
|
|
13
|
+
- **`client.run()` text-mode input fix.** `str` input together with a text
|
|
14
|
+
`encoding`/`errors` used to be pre-encoded to bytes *and* handed to
|
|
15
|
+
`subprocess.run` with the encoding, which then tried to `.encode()` bytes and
|
|
16
|
+
crashed. Text mode now keeps `str` as-is (and decodes `bytes` input); binary
|
|
17
|
+
mode encodes. Found by live testing against a real TrueNAS 26.0 host — the
|
|
18
|
+
unit tests mock `subprocess.run`, so only a real POSIX run exercised it.
|
|
19
|
+
- **`pytruenas.__version__`** now exposed (resolved from installed metadata).
|
|
20
|
+
- **Dependency floors** raised to the tested versions: `duho>=0.3.2` (CLI parser
|
|
21
|
+
fixes) and the `ssh` extra's `pathlib_next[sftp-async]>=0.8.3` (SFTP
|
|
22
|
+
concurrency 8→16).
|
|
23
|
+
- **Repo brought to standard:** benchmark suite (`benchmarks/run.py`), docs site
|
|
24
|
+
(`mkdocs.yml` + `docs/`), `CONTRIBUTING.md`, and this file.
|
|
25
|
+
|
|
26
|
+
### Performance
|
|
27
|
+
|
|
28
|
+
First benchmark baseline (see `benchmarks/results/`). These are **local**
|
|
29
|
+
numbers (Intel i7-1065G7 class, Windows) for regression-catching, not a CI
|
|
30
|
+
figure. The suite measures pure-CPU hot paths — no network.
|
|
31
|
+
|
|
32
|
+
| metric | py3.9 median (ms/call) | py3.14 median (ms/call) |
|
|
33
|
+
| --- | --- | --- |
|
|
34
|
+
| `ejson.dumps.plain` (20-row response) | 0.048 | 0.045 |
|
|
35
|
+
| `ejson.loads.plain` | 0.048 | 0.050 |
|
|
36
|
+
| `ejson.dumps.extended` | 0.020 | 0.021 |
|
|
37
|
+
| `ejson.loads.extended` | 0.020 | 0.020 |
|
|
38
|
+
| `namespace.methodname` | 0.004 | 0.004 |
|
|
39
|
+
| `query.filter_from_kwargs` | 0.003 | 0.002 |
|
|
40
|
+
|
|
41
|
+
**Hot-path analysis:** the ejson encode/decode paths dominate and are already
|
|
42
|
+
near stdlib-optimal. Two candidate micro-optimizations were measured and
|
|
43
|
+
**rejected** (each ~1.02x, within noise): swapping `json.dumps(cls=Encoder)` for
|
|
44
|
+
`default=func` (Python's `json` uses the pure-Python encoder whenever any custom
|
|
45
|
+
type handling is present, regardless), and a fast-path bail in `_object_hook`
|
|
46
|
+
(real response dicts are multi-key, already short-circuited). The client's cost
|
|
47
|
+
is I/O-bound; there is no pending CPU speedup. This baseline exists to catch a
|
|
48
|
+
future regression.
|
|
49
|
+
|
|
50
|
+
**Performance target for the next release:** no regression on the medians above
|
|
51
|
+
(compare same machine + interpreter).
|
|
52
|
+
|
|
53
|
+
### Validation evidence
|
|
54
|
+
|
|
55
|
+
- Tests: **113 passed / 5 skipped** on Windows Python 3.9; **118 passed / 0
|
|
56
|
+
skipped** on Python 3.13 on a real TrueNAS 26.0 host (the POSIX-shell-gated
|
|
57
|
+
`run()` tests that skip on Windows run there).
|
|
58
|
+
- Live integration on TrueNAS 26.0.0-BETA.1 via the local middleware socket:
|
|
59
|
+
`system.info`, `user.query`, `pool.dataset.query`, `_get`, fs `Path`
|
|
60
|
+
exists/read, `client.run()` shell paths, and the `dump-api` / `query` /
|
|
61
|
+
`generate-typings` CLI subcommands (129 valid `.pyi` stubs generated).
|
|
62
|
+
- Build: `python -m build` + `twine check` PASS; wheel carries `py.typed`, no
|
|
63
|
+
agent-file leakage.
|
|
64
|
+
- Docs: `mkdocs build --strict` clean.
|
|
65
|
+
- Coverage: TOTAL 70% (client 62%, namespace 79%, jsonrpc 84%, main 80%).
|
|
66
|
+
|
|
67
|
+
### Publication state
|
|
68
|
+
|
|
69
|
+
Prepared and merged to `main`. **Not tagged/published** — a `v0.1.0` release is
|
|
70
|
+
user-gated and requires PyPI trusted-publishing + GitHub Pages setup by the repo
|
|
71
|
+
owner.
|
|
72
|
+
|
|
73
|
+
---
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# pytruenas benchmarks
|
|
2
|
+
|
|
3
|
+
Micro-benchmarks for the pure-CPU hot paths hit on every middleware call —
|
|
4
|
+
extended-JSON (ejson) encode/decode, API namespace method-name building, and
|
|
5
|
+
query-filter construction. **No network or server is involved**; these measure
|
|
6
|
+
per-call CPU cost, not the round-trip latency that dominates real use.
|
|
7
|
+
|
|
8
|
+
## Running
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
# print summary only
|
|
12
|
+
PYTHONPATH=src .venv39/Scripts/python benchmarks/run.py
|
|
13
|
+
|
|
14
|
+
# also write benchmarks/results/pytruenas-<ver>-py<ver>.json
|
|
15
|
+
PYTHONPATH=src .venv39/Scripts/python benchmarks/run.py --save
|
|
16
|
+
PYTHONPATH=src .venv314/Scripts/python benchmarks/run.py --save
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Run without `--save` to just print. `--name <x>` overrides the filename stem.
|
|
20
|
+
Compare files only across the **same machine + interpreter**, or the numbers
|
|
21
|
+
aren't meaningful.
|
|
22
|
+
|
|
23
|
+
## Schema
|
|
24
|
+
|
|
25
|
+
Each `results/<name>.json`: `name`, `pytruenas_version`, `python`, `platform`,
|
|
26
|
+
`processor`, `timestamp` (UTC), `iterations` (inner counts + `repeat`), and
|
|
27
|
+
`metrics`. Each metric reports `min_ms`/`median_ms`/`max_ms` **per call**,
|
|
28
|
+
sampled `repeat` times — compare on `median_ms`; min/max show run-to-run noise.
|
|
29
|
+
|
|
30
|
+
Metrics:
|
|
31
|
+
- `ejson.dumps.plain` / `ejson.loads.plain` — a 20-row `user.query`-shaped
|
|
32
|
+
response with no extended types (the common case).
|
|
33
|
+
- `ejson.dumps.extended` / `ejson.loads.extended` — a payload exercising every
|
|
34
|
+
extended type (datetime/date/time/set/IPv4Interface).
|
|
35
|
+
- `namespace.methodname` — building `pool.dataset.snapshot` from an attribute
|
|
36
|
+
walk (fresh chain each call, so the `@cache` hit is not what's measured).
|
|
37
|
+
- `query.filter_from_kwargs` — turning `username="root", uid=GT(0), locked=False`
|
|
38
|
+
into middleware filter tuples.
|
|
39
|
+
|
|
40
|
+
## Findings (2026-07-19, baseline)
|
|
41
|
+
|
|
42
|
+
The ejson paths dominate (~0.02–0.05 ms/call) and are **already near
|
|
43
|
+
stdlib-optimal**. Two candidate optimizations were measured and rejected:
|
|
44
|
+
|
|
45
|
+
- Swapping `json.dumps(cls=Encoder)` for `json.dumps(default=func)` to avoid
|
|
46
|
+
re-instantiating the encoder: **~1.02x**, within noise. Python's `json` drops
|
|
47
|
+
to the pure-Python `iterencode` whenever any custom type handling is present,
|
|
48
|
+
regardless of `cls=` vs `default=`, so there's no C-encoder fast path to
|
|
49
|
+
recover here.
|
|
50
|
+
- A fast-path bail in `_object_hook` for single-key dicts whose key doesn't
|
|
51
|
+
start with `$`: **~1.02x**. Real response dicts are multi-key, so the existing
|
|
52
|
+
`len(obj) == 1` guard already short-circuits them.
|
|
53
|
+
|
|
54
|
+
Conclusion: the client's CPU hot paths are not a bottleneck worth
|
|
55
|
+
micro-optimizing; real-world cost is I/O-bound (network round-trips). This
|
|
56
|
+
baseline exists to catch a future *regression*, not because a speedup is
|
|
57
|
+
pending. The per-release before/after narrative lives in the repo-root
|
|
58
|
+
`RELEASENOTES.md`.
|