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.
Files changed (83) hide show
  1. pytruenas-0.0.0/.gitignore +43 -0
  2. pytruenas-0.0.0/CHANGELOG.md +104 -0
  3. pytruenas-0.0.0/CONTRIBUTING.md +87 -0
  4. pytruenas-0.0.0/LICENSE +21 -0
  5. pytruenas-0.0.0/PKG-INFO +182 -0
  6. pytruenas-0.0.0/README.md +106 -0
  7. pytruenas-0.0.0/RELEASENOTES.md +73 -0
  8. pytruenas-0.0.0/benchmarks/README.md +58 -0
  9. pytruenas-0.0.0/benchmarks/results/pytruenas-0.1.0-py314.json +46 -0
  10. pytruenas-0.0.0/benchmarks/results/pytruenas-0.1.0-py39.json +46 -0
  11. pytruenas-0.0.0/benchmarks/run.py +129 -0
  12. pytruenas-0.0.0/docs/api/auth.md +3 -0
  13. pytruenas-0.0.0/docs/api/client.md +3 -0
  14. pytruenas-0.0.0/docs/api/fs.md +3 -0
  15. pytruenas-0.0.0/docs/api/jsonrpc.md +11 -0
  16. pytruenas-0.0.0/docs/api/namespace.md +5 -0
  17. pytruenas-0.0.0/docs/api/reference.md +12 -0
  18. pytruenas-0.0.0/docs/changelog.md +3 -0
  19. pytruenas-0.0.0/docs/guide/api.md +57 -0
  20. pytruenas-0.0.0/docs/guide/cli.md +70 -0
  21. pytruenas-0.0.0/docs/guide/commands.md +30 -0
  22. pytruenas-0.0.0/docs/guide/connecting.md +43 -0
  23. pytruenas-0.0.0/docs/guide/filesystem.md +33 -0
  24. pytruenas-0.0.0/docs/guide/typings.md +28 -0
  25. pytruenas-0.0.0/docs/index.md +58 -0
  26. pytruenas-0.0.0/mkdocs.yml +63 -0
  27. pytruenas-0.0.0/pyproject.toml +85 -0
  28. pytruenas-0.0.0/src/pytruenas/AGENTS.md +319 -0
  29. pytruenas-0.0.0/src/pytruenas/__init__.py +13 -0
  30. pytruenas-0.0.0/src/pytruenas/__main__.py +7 -0
  31. pytruenas-0.0.0/src/pytruenas/_conn.py +21 -0
  32. pytruenas-0.0.0/src/pytruenas/auth.py +126 -0
  33. pytruenas-0.0.0/src/pytruenas/client.py +471 -0
  34. pytruenas-0.0.0/src/pytruenas/cmd/call.py +60 -0
  35. pytruenas-0.0.0/src/pytruenas/cmd/dump_api.py +20 -0
  36. pytruenas-0.0.0/src/pytruenas/cmd/generate_typings.py +74 -0
  37. pytruenas-0.0.0/src/pytruenas/cmd/query.py +47 -0
  38. pytruenas-0.0.0/src/pytruenas/codegen/__init__.py +502 -0
  39. pytruenas-0.0.0/src/pytruenas/codegen/jinja.py +18 -0
  40. pytruenas-0.0.0/src/pytruenas/codegen/namespace.pyi.j2 +37 -0
  41. pytruenas-0.0.0/src/pytruenas/fs/__init__.py +65 -0
  42. pytruenas-0.0.0/src/pytruenas/fs/tnasws.py +201 -0
  43. pytruenas-0.0.0/src/pytruenas/fs/truenas.py +211 -0
  44. pytruenas-0.0.0/src/pytruenas/jsonrpc.py +414 -0
  45. pytruenas-0.0.0/src/pytruenas/main.py +173 -0
  46. pytruenas-0.0.0/src/pytruenas/models/__init__.py +0 -0
  47. pytruenas-0.0.0/src/pytruenas/models/apidump.py +39 -0
  48. pytruenas-0.0.0/src/pytruenas/models/jsonschema.py +237 -0
  49. pytruenas-0.0.0/src/pytruenas/namespace.py +331 -0
  50. pytruenas-0.0.0/src/pytruenas/ops/__init__.py +32 -0
  51. pytruenas-0.0.0/src/pytruenas/ops/host.py +148 -0
  52. pytruenas-0.0.0/src/pytruenas/ops/main.py +71 -0
  53. pytruenas-0.0.0/src/pytruenas/ops/midclt.py +229 -0
  54. pytruenas-0.0.0/src/pytruenas/ops/template.py +124 -0
  55. pytruenas-0.0.0/src/pytruenas/py.typed +0 -0
  56. pytruenas-0.0.0/src/pytruenas/utils/__init__.py +0 -0
  57. pytruenas-0.0.0/src/pytruenas/utils/async_.py +90 -0
  58. pytruenas-0.0.0/src/pytruenas/utils/cmd.py +147 -0
  59. pytruenas-0.0.0/src/pytruenas/utils/io.py +21 -0
  60. pytruenas-0.0.0/src/pytruenas/utils/query.py +121 -0
  61. pytruenas-0.0.0/src/pytruenas/utils/target.py +87 -0
  62. pytruenas-0.0.0/tests/fixtures/api_min.json +132 -0
  63. pytruenas-0.0.0/tests/test_async.py +64 -0
  64. pytruenas-0.0.0/tests/test_auth.py +70 -0
  65. pytruenas-0.0.0/tests/test_cli_call.py +62 -0
  66. pytruenas-0.0.0/tests/test_cli_targets.py +64 -0
  67. pytruenas-0.0.0/tests/test_client.py +89 -0
  68. pytruenas-0.0.0/tests/test_client_more.py +199 -0
  69. pytruenas-0.0.0/tests/test_cmd_config.py +38 -0
  70. pytruenas-0.0.0/tests/test_codegen.py +101 -0
  71. pytruenas-0.0.0/tests/test_fs_local.py +48 -0
  72. pytruenas-0.0.0/tests/test_fs_tnas.py +116 -0
  73. pytruenas-0.0.0/tests/test_jsonrpc.py +102 -0
  74. pytruenas-0.0.0/tests/test_jsonrpc_client.py +192 -0
  75. pytruenas-0.0.0/tests/test_main.py +112 -0
  76. pytruenas-0.0.0/tests/test_namespace.py +174 -0
  77. pytruenas-0.0.0/tests/test_namespace_cache.py +50 -0
  78. pytruenas-0.0.0/tests/test_namespace_retry.py +85 -0
  79. pytruenas-0.0.0/tests/test_ops_midclt.py +27 -0
  80. pytruenas-0.0.0/tests/test_ops_template.py +83 -0
  81. pytruenas-0.0.0/tests/test_package.py +17 -0
  82. pytruenas-0.0.0/tests/test_query.py +37 -0
  83. 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.
@@ -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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/pytruenas.svg)](https://pypi.org/project/pytruenas/)
80
+ [![Python versions](https://img.shields.io/pypi/pyversions/pytruenas.svg)](https://pypi.org/project/pytruenas/)
81
+ [![Documentation](https://img.shields.io/badge/docs-jose--pr.github.io%2Fpytruenas-blue.svg)](https://jose-pr.github.io/pytruenas/)
82
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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
+ [![PyPI version](https://img.shields.io/pypi/v/pytruenas.svg)](https://pypi.org/project/pytruenas/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/pytruenas.svg)](https://pypi.org/project/pytruenas/)
5
+ [![Documentation](https://img.shields.io/badge/docs-jose--pr.github.io%2Fpytruenas-blue.svg)](https://jose-pr.github.io/pytruenas/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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`.