fnox-py 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.
- fnox_py-0.1.0/PKG-INFO +287 -0
- fnox_py-0.1.0/README.md +277 -0
- fnox_py-0.1.0/pyproject.toml +104 -0
- fnox_py-0.1.0/src/fnox_py/__init__.py +31 -0
- fnox_py-0.1.0/src/fnox_py/api.py +115 -0
- fnox_py-0.1.0/src/fnox_py/binary.py +94 -0
- fnox_py-0.1.0/src/fnox_py/cli.py +62 -0
- fnox_py-0.1.0/src/fnox_py/errors.py +24 -0
- fnox_py-0.1.0/src/fnox_py/py.typed +0 -0
- fnox_py-0.1.0/src/fnox_py/runner.py +90 -0
fnox_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fnox-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python bindings for fnox
|
|
5
|
+
Author: Zach Fuller
|
|
6
|
+
Author-email: Zach Fuller <zach.fuller1222@gmail.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# fnox-py
|
|
12
|
+
|
|
13
|
+
`fnox-py` is a thin Python wrapper around the [`fnox`](https://github.com/jdx/fnox) binary.
|
|
14
|
+
|
|
15
|
+
It does not reimplement `fnox` behavior in Python. Instead, it:
|
|
16
|
+
|
|
17
|
+
- locates a real `fnox` binary
|
|
18
|
+
- builds argv for common commands
|
|
19
|
+
- runs the binary
|
|
20
|
+
- returns parsed results or typed errors
|
|
21
|
+
|
|
22
|
+
Python requirement: `>=3.12`
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
### pip
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install fnox-py
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### uv
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv add fnox-py
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
If you want the CLI available outside a project environment, you can also use a tool install:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install fnox-py
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Bundled binary vs source install
|
|
45
|
+
|
|
46
|
+
Platform wheels are intended to bundle the `fnox` binary.
|
|
47
|
+
|
|
48
|
+
If you install from source instead of a platform wheel, `fnox-py` requires a real `fnox` executable to be available via:
|
|
49
|
+
|
|
50
|
+
- `PATH`, or
|
|
51
|
+
- `FNOX_PY_BINARY=/absolute/path/to/fnox`
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install --no-binary fnox-py fnox-py
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
FNOX_PY_BINARY=/usr/local/bin/fnox python -c "import fnox_py; print(fnox_py.version())"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Binary Resolution
|
|
64
|
+
|
|
65
|
+
At runtime, `fnox-py` resolves the `fnox` binary in this order:
|
|
66
|
+
|
|
67
|
+
1. `FNOX_PY_BINARY`
|
|
68
|
+
2. bundled/installed locations in the current environment
|
|
69
|
+
3. bundled/installed fallback locations associated with the base or target install
|
|
70
|
+
4. user scheme script location
|
|
71
|
+
5. `PATH`
|
|
72
|
+
|
|
73
|
+
If `FNOX_PY_BINARY` is set but points to a missing file, `fnox-py` raises `FnoxNotFoundError`.
|
|
74
|
+
|
|
75
|
+
## Python API
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from fnox_py import (
|
|
79
|
+
config_files,
|
|
80
|
+
export_json,
|
|
81
|
+
get,
|
|
82
|
+
lease_create,
|
|
83
|
+
profiles,
|
|
84
|
+
providers,
|
|
85
|
+
schema,
|
|
86
|
+
version,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
value = get("MY_SECRET")
|
|
90
|
+
all_values = export_json()
|
|
91
|
+
schema_doc = schema()
|
|
92
|
+
profile_names = profiles()
|
|
93
|
+
provider_names = providers()
|
|
94
|
+
config_paths = config_files()
|
|
95
|
+
lease = lease_create("vault", duration="1h", label="local-dev")
|
|
96
|
+
fnox_version = version()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Common examples
|
|
100
|
+
|
|
101
|
+
Get a single value:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from fnox_py import get
|
|
105
|
+
|
|
106
|
+
token = get("API_TOKEN")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Get a value from a specific profile:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from fnox_py import get
|
|
113
|
+
|
|
114
|
+
token = get("API_TOKEN", profile="prod")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Decode base64 output:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from fnox_py import get
|
|
121
|
+
|
|
122
|
+
decoded = get("TLS_CERT", base64_decode=True)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Export all secrets as JSON:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from fnox_py import export_json
|
|
129
|
+
|
|
130
|
+
data = export_json(profile="dev")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Inspect schema, profiles, providers, and config files:
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from fnox_py import config_files, profiles, providers, schema
|
|
137
|
+
|
|
138
|
+
print(schema())
|
|
139
|
+
print(profiles())
|
|
140
|
+
print(providers())
|
|
141
|
+
print(config_files())
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Create a lease:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from fnox_py import lease_create
|
|
148
|
+
|
|
149
|
+
lease = lease_create("vault", duration="30m", label="ci-job")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Get the underlying `fnox` version:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from fnox_py import version
|
|
156
|
+
|
|
157
|
+
print(version())
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## CLI
|
|
161
|
+
|
|
162
|
+
The package installs the `fnox-py` console script.
|
|
163
|
+
|
|
164
|
+
### Built-in subcommands
|
|
165
|
+
|
|
166
|
+
Locate the resolved binary:
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
fnox-py which
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Show the wrapper version and attempt to print the underlying `fnox` version:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
fnox-py version
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Print basic environment diagnostics:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
fnox-py doctor
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Passthrough behavior
|
|
185
|
+
|
|
186
|
+
Any arguments other than `which`, `version`, and `doctor` are passed directly through to `fnox`.
|
|
187
|
+
|
|
188
|
+
For example:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
fnox-py get MY_SECRET
|
|
192
|
+
fnox-py profiles
|
|
193
|
+
fnox-py export --format json
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
With no arguments, `fnox-py` runs `fnox` with no extra argv.
|
|
197
|
+
|
|
198
|
+
## Public API
|
|
199
|
+
|
|
200
|
+
`fnox-py` currently exports:
|
|
201
|
+
|
|
202
|
+
- `config_files`
|
|
203
|
+
- `export_json`
|
|
204
|
+
- `get`
|
|
205
|
+
- `lease_create`
|
|
206
|
+
- `profiles`
|
|
207
|
+
- `providers`
|
|
208
|
+
- `schema`
|
|
209
|
+
- `version`
|
|
210
|
+
- `find_fnox_bin`
|
|
211
|
+
- `run`
|
|
212
|
+
- `FnoxResult`
|
|
213
|
+
- `FnoxCommandError`
|
|
214
|
+
- `FnoxError`
|
|
215
|
+
- `FnoxNotFoundError`
|
|
216
|
+
- `FnoxTimeoutError`
|
|
217
|
+
|
|
218
|
+
## Errors
|
|
219
|
+
|
|
220
|
+
Library calls raise typed exceptions:
|
|
221
|
+
|
|
222
|
+
- `FnoxNotFoundError` when the binary cannot be found
|
|
223
|
+
- `FnoxCommandError` when `fnox` exits non-zero
|
|
224
|
+
- `FnoxTimeoutError` on subprocess timeout
|
|
225
|
+
- `FnoxError` as the base exception type
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
This project uses `uv`, `pytest`, `ruff`, and `mypy`.
|
|
230
|
+
|
|
231
|
+
Install dependencies:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
uv sync
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Run tests:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
uv run pytest -v
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Run a single test:
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
uv run pytest tests/test_api.py::test_get -q
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Lint:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
uv run ruff check src tests scripts
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Type-check:
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
uv run mypy src
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Build distributions:
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
uv build
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Release / Platform Wheel Build
|
|
268
|
+
|
|
269
|
+
`scripts/build_platform_wheel.py` builds platform-specific wheels by:
|
|
270
|
+
|
|
271
|
+
1. building a pure Python wheel
|
|
272
|
+
2. downloading upstream `fnox` release binaries
|
|
273
|
+
3. injecting the binary into the wheel
|
|
274
|
+
4. rewriting wheel metadata
|
|
275
|
+
5. building an sdist
|
|
276
|
+
|
|
277
|
+
Example:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
uv run python scripts/build_platform_wheel.py --fnox-version 1.0.0 --output dist/
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Notes
|
|
284
|
+
|
|
285
|
+
- `fnox-py` is intentionally small and wrapper-focused.
|
|
286
|
+
- For behavior, flags, and command semantics, prefer the upstream `fnox` documentation.
|
|
287
|
+
- If you need lower-level control, use `run()` directly and inspect `FnoxResult`.
|
fnox_py-0.1.0/README.md
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# fnox-py
|
|
2
|
+
|
|
3
|
+
`fnox-py` is a thin Python wrapper around the [`fnox`](https://github.com/jdx/fnox) binary.
|
|
4
|
+
|
|
5
|
+
It does not reimplement `fnox` behavior in Python. Instead, it:
|
|
6
|
+
|
|
7
|
+
- locates a real `fnox` binary
|
|
8
|
+
- builds argv for common commands
|
|
9
|
+
- runs the binary
|
|
10
|
+
- returns parsed results or typed errors
|
|
11
|
+
|
|
12
|
+
Python requirement: `>=3.12`
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
### pip
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install fnox-py
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### uv
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv add fnox-py
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
If you want the CLI available outside a project environment, you can also use a tool install:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
uv tool install fnox-py
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Bundled binary vs source install
|
|
35
|
+
|
|
36
|
+
Platform wheels are intended to bundle the `fnox` binary.
|
|
37
|
+
|
|
38
|
+
If you install from source instead of a platform wheel, `fnox-py` requires a real `fnox` executable to be available via:
|
|
39
|
+
|
|
40
|
+
- `PATH`, or
|
|
41
|
+
- `FNOX_PY_BINARY=/absolute/path/to/fnox`
|
|
42
|
+
|
|
43
|
+
Examples:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install --no-binary fnox-py fnox-py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
FNOX_PY_BINARY=/usr/local/bin/fnox python -c "import fnox_py; print(fnox_py.version())"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Binary Resolution
|
|
54
|
+
|
|
55
|
+
At runtime, `fnox-py` resolves the `fnox` binary in this order:
|
|
56
|
+
|
|
57
|
+
1. `FNOX_PY_BINARY`
|
|
58
|
+
2. bundled/installed locations in the current environment
|
|
59
|
+
3. bundled/installed fallback locations associated with the base or target install
|
|
60
|
+
4. user scheme script location
|
|
61
|
+
5. `PATH`
|
|
62
|
+
|
|
63
|
+
If `FNOX_PY_BINARY` is set but points to a missing file, `fnox-py` raises `FnoxNotFoundError`.
|
|
64
|
+
|
|
65
|
+
## Python API
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from fnox_py import (
|
|
69
|
+
config_files,
|
|
70
|
+
export_json,
|
|
71
|
+
get,
|
|
72
|
+
lease_create,
|
|
73
|
+
profiles,
|
|
74
|
+
providers,
|
|
75
|
+
schema,
|
|
76
|
+
version,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
value = get("MY_SECRET")
|
|
80
|
+
all_values = export_json()
|
|
81
|
+
schema_doc = schema()
|
|
82
|
+
profile_names = profiles()
|
|
83
|
+
provider_names = providers()
|
|
84
|
+
config_paths = config_files()
|
|
85
|
+
lease = lease_create("vault", duration="1h", label="local-dev")
|
|
86
|
+
fnox_version = version()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Common examples
|
|
90
|
+
|
|
91
|
+
Get a single value:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from fnox_py import get
|
|
95
|
+
|
|
96
|
+
token = get("API_TOKEN")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Get a value from a specific profile:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from fnox_py import get
|
|
103
|
+
|
|
104
|
+
token = get("API_TOKEN", profile="prod")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Decode base64 output:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from fnox_py import get
|
|
111
|
+
|
|
112
|
+
decoded = get("TLS_CERT", base64_decode=True)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Export all secrets as JSON:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from fnox_py import export_json
|
|
119
|
+
|
|
120
|
+
data = export_json(profile="dev")
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Inspect schema, profiles, providers, and config files:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from fnox_py import config_files, profiles, providers, schema
|
|
127
|
+
|
|
128
|
+
print(schema())
|
|
129
|
+
print(profiles())
|
|
130
|
+
print(providers())
|
|
131
|
+
print(config_files())
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Create a lease:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from fnox_py import lease_create
|
|
138
|
+
|
|
139
|
+
lease = lease_create("vault", duration="30m", label="ci-job")
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Get the underlying `fnox` version:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from fnox_py import version
|
|
146
|
+
|
|
147
|
+
print(version())
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## CLI
|
|
151
|
+
|
|
152
|
+
The package installs the `fnox-py` console script.
|
|
153
|
+
|
|
154
|
+
### Built-in subcommands
|
|
155
|
+
|
|
156
|
+
Locate the resolved binary:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
fnox-py which
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Show the wrapper version and attempt to print the underlying `fnox` version:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
fnox-py version
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Print basic environment diagnostics:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
fnox-py doctor
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Passthrough behavior
|
|
175
|
+
|
|
176
|
+
Any arguments other than `which`, `version`, and `doctor` are passed directly through to `fnox`.
|
|
177
|
+
|
|
178
|
+
For example:
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
fnox-py get MY_SECRET
|
|
182
|
+
fnox-py profiles
|
|
183
|
+
fnox-py export --format json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
With no arguments, `fnox-py` runs `fnox` with no extra argv.
|
|
187
|
+
|
|
188
|
+
## Public API
|
|
189
|
+
|
|
190
|
+
`fnox-py` currently exports:
|
|
191
|
+
|
|
192
|
+
- `config_files`
|
|
193
|
+
- `export_json`
|
|
194
|
+
- `get`
|
|
195
|
+
- `lease_create`
|
|
196
|
+
- `profiles`
|
|
197
|
+
- `providers`
|
|
198
|
+
- `schema`
|
|
199
|
+
- `version`
|
|
200
|
+
- `find_fnox_bin`
|
|
201
|
+
- `run`
|
|
202
|
+
- `FnoxResult`
|
|
203
|
+
- `FnoxCommandError`
|
|
204
|
+
- `FnoxError`
|
|
205
|
+
- `FnoxNotFoundError`
|
|
206
|
+
- `FnoxTimeoutError`
|
|
207
|
+
|
|
208
|
+
## Errors
|
|
209
|
+
|
|
210
|
+
Library calls raise typed exceptions:
|
|
211
|
+
|
|
212
|
+
- `FnoxNotFoundError` when the binary cannot be found
|
|
213
|
+
- `FnoxCommandError` when `fnox` exits non-zero
|
|
214
|
+
- `FnoxTimeoutError` on subprocess timeout
|
|
215
|
+
- `FnoxError` as the base exception type
|
|
216
|
+
|
|
217
|
+
## Development
|
|
218
|
+
|
|
219
|
+
This project uses `uv`, `pytest`, `ruff`, and `mypy`.
|
|
220
|
+
|
|
221
|
+
Install dependencies:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
uv sync
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Run tests:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
uv run pytest -v
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
Run a single test:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
uv run pytest tests/test_api.py::test_get -q
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Lint:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
uv run ruff check src tests scripts
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Type-check:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
uv run mypy src
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Build distributions:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
uv build
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Release / Platform Wheel Build
|
|
258
|
+
|
|
259
|
+
`scripts/build_platform_wheel.py` builds platform-specific wheels by:
|
|
260
|
+
|
|
261
|
+
1. building a pure Python wheel
|
|
262
|
+
2. downloading upstream `fnox` release binaries
|
|
263
|
+
3. injecting the binary into the wheel
|
|
264
|
+
4. rewriting wheel metadata
|
|
265
|
+
5. building an sdist
|
|
266
|
+
|
|
267
|
+
Example:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
uv run python scripts/build_platform_wheel.py --fnox-version 1.0.0 --output dist/
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
## Notes
|
|
274
|
+
|
|
275
|
+
- `fnox-py` is intentionally small and wrapper-focused.
|
|
276
|
+
- For behavior, flags, and command semantics, prefer the upstream `fnox` documentation.
|
|
277
|
+
- If you need lower-level control, use `run()` directly and inspect `FnoxResult`.
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fnox-py"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Python bindings for fnox"
|
|
5
|
+
license = "MIT"
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
authors = [{ name = "Zach Fuller", email = "zach.fuller1222@gmail.com" }]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[build-system]
|
|
12
|
+
requires = ["uv_build>=0.10.11,<0.11.0"]
|
|
13
|
+
build-backend = "uv_build"
|
|
14
|
+
|
|
15
|
+
[project.scripts]
|
|
16
|
+
fnox-py = "fnox_py.cli:main"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"mypy>=1.19.1",
|
|
21
|
+
"prek>=0.3.6",
|
|
22
|
+
"ruff>=0.15.6",
|
|
23
|
+
"pytest>=8.0",
|
|
24
|
+
"urllib3>=2.6.3",
|
|
25
|
+
"rich>=14.3.3",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[tool.pyright]
|
|
29
|
+
include = ["src/**", "tests/**"]
|
|
30
|
+
typeCheckingMode = "standard"
|
|
31
|
+
|
|
32
|
+
[tool.mypy]
|
|
33
|
+
warn_redundant_casts = true
|
|
34
|
+
warn_unused_ignores = true
|
|
35
|
+
disallow_any_generics = true
|
|
36
|
+
check_untyped_defs = true
|
|
37
|
+
no_implicit_reexport = true
|
|
38
|
+
disallow_untyped_defs = true
|
|
39
|
+
local_partial_types = true
|
|
40
|
+
warn_return_any = true
|
|
41
|
+
enable_error_code = [
|
|
42
|
+
"ignore-without-code",
|
|
43
|
+
"no-untyped-def",
|
|
44
|
+
"unreachable",
|
|
45
|
+
"unused-awaitable",
|
|
46
|
+
"explicit-override",
|
|
47
|
+
"mutable-override",
|
|
48
|
+
"exhaustive-match",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
include = ["src/**", "tests/**", "scripts/**"]
|
|
53
|
+
exclude = ["tests/data/*.txt", ".venv"]
|
|
54
|
+
line-length = 120
|
|
55
|
+
indent-width = 4
|
|
56
|
+
|
|
57
|
+
target-version = "py313"
|
|
58
|
+
|
|
59
|
+
[tool.ruff.lint]
|
|
60
|
+
select = [
|
|
61
|
+
"E",
|
|
62
|
+
"F",
|
|
63
|
+
"W",
|
|
64
|
+
"C90",
|
|
65
|
+
"I",
|
|
66
|
+
"N",
|
|
67
|
+
"UP",
|
|
68
|
+
"ASYNC",
|
|
69
|
+
"S",
|
|
70
|
+
"B",
|
|
71
|
+
"ERA",
|
|
72
|
+
"PLE",
|
|
73
|
+
"PLW",
|
|
74
|
+
"PLC",
|
|
75
|
+
"PLW",
|
|
76
|
+
"PERF",
|
|
77
|
+
"RUF",
|
|
78
|
+
"SIM",
|
|
79
|
+
"PT",
|
|
80
|
+
"T20",
|
|
81
|
+
"PTH",
|
|
82
|
+
"LOG",
|
|
83
|
+
"G",
|
|
84
|
+
]
|
|
85
|
+
ignore = ["E501", "S101", "PLC0415"]
|
|
86
|
+
|
|
87
|
+
# Allow fix for all enabled rules (when `--fix`) is provided.
|
|
88
|
+
fixable = ["ALL"]
|
|
89
|
+
unfixable = []
|
|
90
|
+
|
|
91
|
+
# Allow unused variables when underscore-prefixed.
|
|
92
|
+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
|
93
|
+
|
|
94
|
+
[tool.ruff.lint.per-file-ignores]
|
|
95
|
+
"scripts/*" = ["S603", "S607", "S202", "T201"]
|
|
96
|
+
|
|
97
|
+
[tool.ruff.format]
|
|
98
|
+
quote-style = "double"
|
|
99
|
+
indent-style = "space"
|
|
100
|
+
skip-magic-trailing-comma = false
|
|
101
|
+
line-ending = "auto"
|
|
102
|
+
|
|
103
|
+
[tool.ty.rules]
|
|
104
|
+
possibly-unresolved-reference = "warn"
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from .api import (
|
|
2
|
+
config_files,
|
|
3
|
+
export_json,
|
|
4
|
+
get,
|
|
5
|
+
lease_create,
|
|
6
|
+
profiles,
|
|
7
|
+
providers,
|
|
8
|
+
schema,
|
|
9
|
+
version,
|
|
10
|
+
)
|
|
11
|
+
from .binary import find_fnox_bin
|
|
12
|
+
from .errors import FnoxCommandError, FnoxError, FnoxNotFoundError, FnoxTimeoutError
|
|
13
|
+
from .runner import FnoxResult, run
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"FnoxCommandError",
|
|
17
|
+
"FnoxError",
|
|
18
|
+
"FnoxNotFoundError",
|
|
19
|
+
"FnoxResult",
|
|
20
|
+
"FnoxTimeoutError",
|
|
21
|
+
"config_files",
|
|
22
|
+
"export_json",
|
|
23
|
+
"find_fnox_bin",
|
|
24
|
+
"get",
|
|
25
|
+
"lease_create",
|
|
26
|
+
"profiles",
|
|
27
|
+
"providers",
|
|
28
|
+
"run",
|
|
29
|
+
"schema",
|
|
30
|
+
"version",
|
|
31
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from . import runner
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get(
|
|
12
|
+
key: str,
|
|
13
|
+
*,
|
|
14
|
+
profile: str | None = None,
|
|
15
|
+
base64_decode: bool = False,
|
|
16
|
+
env: Mapping[str, str] | None = None,
|
|
17
|
+
cwd: str | Path | None = None,
|
|
18
|
+
timeout: float | None = None,
|
|
19
|
+
) -> str:
|
|
20
|
+
"""Get a single secret value by key."""
|
|
21
|
+
args: list[str] = ["get"]
|
|
22
|
+
if profile is not None:
|
|
23
|
+
args.extend(["--profile", profile])
|
|
24
|
+
if base64_decode:
|
|
25
|
+
args.append("--base64-decode")
|
|
26
|
+
args.append(key)
|
|
27
|
+
result = runner.run(args, env=env, cwd=cwd, timeout=timeout)
|
|
28
|
+
return result.stdout.rstrip("\n")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def export_json(
|
|
32
|
+
*,
|
|
33
|
+
profile: str | None = None,
|
|
34
|
+
env: Mapping[str, str] | None = None,
|
|
35
|
+
cwd: str | Path | None = None,
|
|
36
|
+
timeout: float | None = None,
|
|
37
|
+
) -> dict[str, str]:
|
|
38
|
+
"""Export all secrets as a JSON dictionary."""
|
|
39
|
+
args: list[str] = ["export"]
|
|
40
|
+
if profile is not None:
|
|
41
|
+
args.extend(["--profile", profile])
|
|
42
|
+
args.extend(["--format", "json"])
|
|
43
|
+
result = runner.run(args, env=env, cwd=cwd, timeout=timeout)
|
|
44
|
+
return json.loads(result.stdout) # type: ignore[no-any-return]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def schema(
|
|
48
|
+
*,
|
|
49
|
+
timeout: float | None = None,
|
|
50
|
+
) -> dict[str, Any]:
|
|
51
|
+
"""Return the fnox JSON schema."""
|
|
52
|
+
result = runner.run(["schema"], timeout=timeout)
|
|
53
|
+
return json.loads(result.stdout) # type: ignore[no-any-return]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def profiles(
|
|
57
|
+
*,
|
|
58
|
+
env: Mapping[str, str] | None = None,
|
|
59
|
+
cwd: str | Path | None = None,
|
|
60
|
+
timeout: float | None = None,
|
|
61
|
+
) -> list[str]:
|
|
62
|
+
"""List available profiles."""
|
|
63
|
+
result = runner.run(["profiles"], env=env, cwd=cwd, timeout=timeout)
|
|
64
|
+
return result.stdout.strip().splitlines()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def providers(
|
|
68
|
+
*,
|
|
69
|
+
env: Mapping[str, str] | None = None,
|
|
70
|
+
cwd: str | Path | None = None,
|
|
71
|
+
timeout: float | None = None,
|
|
72
|
+
) -> list[str]:
|
|
73
|
+
"""List available providers."""
|
|
74
|
+
result = runner.run(["providers"], env=env, cwd=cwd, timeout=timeout)
|
|
75
|
+
return result.stdout.strip().splitlines()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def config_files(
|
|
79
|
+
*,
|
|
80
|
+
env: Mapping[str, str] | None = None,
|
|
81
|
+
cwd: str | Path | None = None,
|
|
82
|
+
timeout: float | None = None,
|
|
83
|
+
) -> list[str]:
|
|
84
|
+
"""List config file paths."""
|
|
85
|
+
result = runner.run(["config-files"], env=env, cwd=cwd, timeout=timeout)
|
|
86
|
+
return result.stdout.strip().splitlines()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def lease_create(
|
|
90
|
+
backend: str,
|
|
91
|
+
*,
|
|
92
|
+
duration: str | None = None,
|
|
93
|
+
label: str | None = None,
|
|
94
|
+
env: Mapping[str, str] | None = None,
|
|
95
|
+
cwd: str | Path | None = None,
|
|
96
|
+
timeout: float | None = None,
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
"""Create a lease and return its metadata."""
|
|
99
|
+
args: list[str] = ["lease", "create"]
|
|
100
|
+
if duration is not None:
|
|
101
|
+
args.extend(["--duration", duration])
|
|
102
|
+
if label is not None:
|
|
103
|
+
args.extend(["--label", label])
|
|
104
|
+
args.extend(["--format", "json", backend])
|
|
105
|
+
result = runner.run(args, env=env, cwd=cwd, timeout=timeout)
|
|
106
|
+
return json.loads(result.stdout) # type: ignore[no-any-return]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def version(
|
|
110
|
+
*,
|
|
111
|
+
timeout: float | None = None,
|
|
112
|
+
) -> str:
|
|
113
|
+
"""Return the fnox version string."""
|
|
114
|
+
result = runner.run(["version"], timeout=timeout)
|
|
115
|
+
return result.stdout.strip()
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Binary discovery for fnox.
|
|
2
|
+
#
|
|
3
|
+
# Resolution order mirrors prek (MIT, Astral Software Inc.) with an
|
|
4
|
+
# additional env-var override and PATH fallback.
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import sys
|
|
11
|
+
import sysconfig
|
|
12
|
+
from fnmatch import fnmatch
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from .errors import FnoxNotFoundError
|
|
16
|
+
|
|
17
|
+
_MODULE_DIR = str(Path(__file__).parent)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_fnox_bin() -> str:
|
|
21
|
+
"""Return the path to the fnox binary."""
|
|
22
|
+
|
|
23
|
+
# 1. Env-var override (hard error if set but missing)
|
|
24
|
+
env_path = os.environ.get("FNOX_PY_BINARY")
|
|
25
|
+
if env_path is not None:
|
|
26
|
+
if Path(env_path).is_file():
|
|
27
|
+
return env_path
|
|
28
|
+
raise FnoxNotFoundError(f"FNOX_PY_BINARY is set to {env_path!r} but the file does not exist")
|
|
29
|
+
|
|
30
|
+
fnox_exe = "fnox" + sysconfig.get_config_var("EXE")
|
|
31
|
+
|
|
32
|
+
targets: list[str | None] = [
|
|
33
|
+
# 2. Current venv scripts
|
|
34
|
+
sysconfig.get_path("scripts"),
|
|
35
|
+
# 3. Base prefix scripts
|
|
36
|
+
sysconfig.get_path("scripts", vars={"base": sys.base_prefix}),
|
|
37
|
+
# 4. Parent-of-package-root (platform-aware)
|
|
38
|
+
(
|
|
39
|
+
_join(_matching_parents(_MODULE_DIR, "Lib/site-packages/fnox_py"), "Scripts")
|
|
40
|
+
if sys.platform == "win32"
|
|
41
|
+
else _join(_matching_parents(_MODULE_DIR, "lib/python*/site-packages/fnox_py"), "bin")
|
|
42
|
+
),
|
|
43
|
+
# 5. Adjacent-to-package-root (--target installs)
|
|
44
|
+
_join(_matching_parents(_MODULE_DIR, "fnox_py"), "bin"),
|
|
45
|
+
# 6. User scheme scripts
|
|
46
|
+
sysconfig.get_path("scripts", scheme=sysconfig.get_preferred_scheme("user")),
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
seen: list[str] = []
|
|
50
|
+
for target in targets:
|
|
51
|
+
if not target:
|
|
52
|
+
continue
|
|
53
|
+
if target in seen:
|
|
54
|
+
continue
|
|
55
|
+
seen.append(target)
|
|
56
|
+
candidate = Path(target) / fnox_exe
|
|
57
|
+
if candidate.is_file():
|
|
58
|
+
return str(candidate)
|
|
59
|
+
|
|
60
|
+
# 7. PATH fallback (for sdist installs without bundled binary)
|
|
61
|
+
which = shutil.which("fnox")
|
|
62
|
+
if which is not None:
|
|
63
|
+
return which
|
|
64
|
+
|
|
65
|
+
locations = "\n".join(f" - {target}" for target in seen)
|
|
66
|
+
raise FnoxNotFoundError(
|
|
67
|
+
f"Could not find the fnox binary in any of the following locations:\n{locations}\n"
|
|
68
|
+
"Install fnox or set the FNOX_PY_BINARY environment variable."
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Helpers (ported from prek, MIT license, Astral Software Inc.)
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _matching_parents(path: str, match: str) -> str | None:
|
|
78
|
+
parts = Path(path).parts
|
|
79
|
+
match_parts = match.split("/")
|
|
80
|
+
if len(parts) < len(match_parts):
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
if not all(
|
|
84
|
+
fnmatch(part, match_part) for part, match_part in zip(reversed(parts), reversed(match_parts), strict=False)
|
|
85
|
+
):
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
return str(Path(*parts[: -len(match_parts)]))
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _join(path: str | None, *parts: str) -> str | None:
|
|
92
|
+
if not path:
|
|
93
|
+
return None
|
|
94
|
+
return str(Path(path).joinpath(*parts))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .binary import find_fnox_bin
|
|
6
|
+
from .errors import FnoxNotFoundError
|
|
7
|
+
from .runner import run, run_passthrough
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
if len(sys.argv) < 2:
|
|
12
|
+
run_passthrough([])
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
subcmd = sys.argv[1]
|
|
16
|
+
|
|
17
|
+
if subcmd == "which":
|
|
18
|
+
_cmd_which()
|
|
19
|
+
elif subcmd == "version":
|
|
20
|
+
_cmd_version()
|
|
21
|
+
elif subcmd == "doctor":
|
|
22
|
+
_cmd_doctor()
|
|
23
|
+
else:
|
|
24
|
+
run_passthrough(sys.argv[1:])
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cmd_which() -> None:
|
|
28
|
+
try:
|
|
29
|
+
path = find_fnox_bin()
|
|
30
|
+
except FnoxNotFoundError as exc:
|
|
31
|
+
print(str(exc), file=sys.stderr) # noqa: T201
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
print(path) # noqa: T201
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _cmd_version() -> None:
|
|
37
|
+
from importlib.metadata import version as pkg_version
|
|
38
|
+
|
|
39
|
+
wrapper_version = pkg_version("fnox-py")
|
|
40
|
+
print(f"fnox-py {wrapper_version}") # noqa: T201
|
|
41
|
+
try:
|
|
42
|
+
result = run(["version"], check=False)
|
|
43
|
+
print(result.stdout.strip()) # noqa: T201
|
|
44
|
+
except FnoxNotFoundError:
|
|
45
|
+
print("fnox binary not found", file=sys.stderr) # noqa: T201
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _cmd_doctor() -> None:
|
|
49
|
+
import shutil
|
|
50
|
+
from importlib.metadata import version as pkg_version
|
|
51
|
+
|
|
52
|
+
print(f"fnox-py {pkg_version('fnox-py')}") # noqa: T201
|
|
53
|
+
print(f"Python {sys.version}") # noqa: T201
|
|
54
|
+
try:
|
|
55
|
+
path = find_fnox_bin()
|
|
56
|
+
print(f"Binary {path}") # noqa: T201
|
|
57
|
+
is_bundled = shutil.which("fnox") != path
|
|
58
|
+
print(f"Bundled {is_bundled}") # noqa: T201
|
|
59
|
+
result = run(["version"], check=False)
|
|
60
|
+
print(f"fnox {result.stdout.strip()}") # noqa: T201
|
|
61
|
+
except FnoxNotFoundError as exc:
|
|
62
|
+
print(f"Binary NOT FOUND: {exc}", file=sys.stderr) # noqa: T201
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class FnoxError(Exception):
|
|
5
|
+
"""Base exception for fnox-py."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FnoxNotFoundError(FnoxError, FileNotFoundError):
|
|
9
|
+
"""The fnox binary could not be found."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class FnoxCommandError(FnoxError):
|
|
13
|
+
"""fnox exited with a non-zero return code."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, returncode: int, stdout: str, stderr: str, cmd: list[str]) -> None:
|
|
16
|
+
self.returncode = returncode
|
|
17
|
+
self.stdout = stdout
|
|
18
|
+
self.stderr = stderr
|
|
19
|
+
self.cmd = cmd
|
|
20
|
+
super().__init__(f"fnox failed with exit code {returncode}: {stderr.strip() or stdout.strip()}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class FnoxTimeoutError(FnoxError):
|
|
24
|
+
"""fnox subprocess timed out."""
|
|
File without changes
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import NoReturn
|
|
10
|
+
|
|
11
|
+
from .binary import find_fnox_bin
|
|
12
|
+
from .errors import FnoxCommandError, FnoxNotFoundError, FnoxTimeoutError
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclasses.dataclass(frozen=True, slots=True)
|
|
16
|
+
class FnoxResult:
|
|
17
|
+
returncode: int
|
|
18
|
+
stdout: str
|
|
19
|
+
stderr: str
|
|
20
|
+
cmd: list[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run(
|
|
24
|
+
args: Sequence[str],
|
|
25
|
+
*,
|
|
26
|
+
env: Mapping[str, str] | None = None,
|
|
27
|
+
cwd: str | Path | None = None,
|
|
28
|
+
check: bool = True,
|
|
29
|
+
timeout: float | None = None,
|
|
30
|
+
input: str | None = None,
|
|
31
|
+
) -> FnoxResult:
|
|
32
|
+
"""Run fnox with the given arguments and return the result."""
|
|
33
|
+
fnox = find_fnox_bin()
|
|
34
|
+
cmd = [fnox, *args]
|
|
35
|
+
|
|
36
|
+
run_env: dict[str, str] | None = None
|
|
37
|
+
if env is not None:
|
|
38
|
+
run_env = {**os.environ, **env}
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
proc = subprocess.run( # noqa: S603
|
|
42
|
+
cmd,
|
|
43
|
+
capture_output=True,
|
|
44
|
+
text=True,
|
|
45
|
+
env=run_env,
|
|
46
|
+
cwd=cwd,
|
|
47
|
+
timeout=timeout,
|
|
48
|
+
input=input,
|
|
49
|
+
check=False,
|
|
50
|
+
)
|
|
51
|
+
except subprocess.TimeoutExpired as exc:
|
|
52
|
+
raise FnoxTimeoutError(f"fnox timed out after {exc.timeout}s") from exc
|
|
53
|
+
except FileNotFoundError as exc:
|
|
54
|
+
raise FnoxNotFoundError(str(exc)) from exc
|
|
55
|
+
|
|
56
|
+
result = FnoxResult(
|
|
57
|
+
returncode=proc.returncode,
|
|
58
|
+
stdout=proc.stdout,
|
|
59
|
+
stderr=proc.stderr,
|
|
60
|
+
cmd=cmd,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if check and proc.returncode != 0:
|
|
64
|
+
raise FnoxCommandError(proc.returncode, proc.stdout, proc.stderr, cmd)
|
|
65
|
+
|
|
66
|
+
return result
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def run_passthrough(
|
|
70
|
+
args: Sequence[str],
|
|
71
|
+
*,
|
|
72
|
+
env: Mapping[str, str] | None = None,
|
|
73
|
+
cwd: str | Path | None = None,
|
|
74
|
+
) -> NoReturn:
|
|
75
|
+
"""Run fnox, forwarding stdio directly. On Unix, replaces the process."""
|
|
76
|
+
fnox = find_fnox_bin()
|
|
77
|
+
cmd = [fnox, *args]
|
|
78
|
+
|
|
79
|
+
if sys.platform == "win32":
|
|
80
|
+
try:
|
|
81
|
+
proc = subprocess.run(cmd, env=env, cwd=cwd, check=False) # noqa: S603
|
|
82
|
+
except KeyboardInterrupt:
|
|
83
|
+
sys.exit(2)
|
|
84
|
+
sys.exit(proc.returncode)
|
|
85
|
+
else:
|
|
86
|
+
if env is not None:
|
|
87
|
+
os.environ.update(env)
|
|
88
|
+
if cwd is not None:
|
|
89
|
+
os.chdir(cwd)
|
|
90
|
+
os.execvp(fnox, cmd) # noqa: S606
|