elva-cli 0.0.4__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elva_cli-0.2.0/PKG-INFO +175 -0
- elva_cli-0.2.0/README.md +148 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/pyproject.toml +1 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/_version.py +2 -2
- elva_cli-0.2.0/src/elva_cli/commands/config.py +25 -0
- elva_cli-0.2.0/src/elva_cli/context.py +148 -0
- elva_cli-0.2.0/src/elva_cli/core/services/config.py +67 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/main.py +52 -9
- elva_cli-0.2.0/src/elva_cli/registry.py +46 -0
- elva_cli-0.2.0/src/elva_cli/settings/__init__.py +6 -0
- elva_cli-0.2.0/src/elva_cli/settings/loader.py +154 -0
- elva_cli-0.2.0/src/elva_cli/settings/models.py +38 -0
- elva_cli-0.2.0/src/elva_cli/settings/paths.py +40 -0
- elva_cli-0.2.0/src/elva_cli/ui/console.py +33 -0
- elva_cli-0.2.0/src/elva_cli/ui/output.py +65 -0
- elva_cli-0.2.0/src/elva_cli/ui/prompts.py +91 -0
- elva_cli-0.2.0/src/elva_cli/ui/renderables/__init__.py +12 -0
- elva_cli-0.2.0/src/elva_cli/ui/renderables/base.py +37 -0
- elva_cli-0.2.0/src/elva_cli/ui/renderables/config.py +41 -0
- elva_cli-0.2.0/src/elva_cli/ui/theme.py +15 -0
- elva_cli-0.2.0/tests/cli/test_config_command.py +84 -0
- elva_cli-0.2.0/tests/cli/test_lazy_imports.py +45 -0
- elva_cli-0.2.0/tests/cli/test_never_blocks.py +124 -0
- elva_cli-0.2.0/tests/cli/test_output_streams.py +110 -0
- elva_cli-0.2.0/tests/unit/test_boundary.py +107 -0
- elva_cli-0.2.0/tests/unit/test_context.py +57 -0
- elva_cli-0.2.0/tests/unit/test_interactivity.py +53 -0
- elva_cli-0.2.0/tests/unit/test_output.py +116 -0
- elva_cli-0.2.0/tests/unit/test_prompts.py +79 -0
- elva_cli-0.2.0/tests/unit/test_settings_loader.py +148 -0
- elva_cli-0.0.4/PKG-INFO +0 -84
- elva_cli-0.0.4/README.md +0 -58
- elva_cli-0.0.4/src/elva_cli/context.py +0 -5
- elva_cli-0.0.4/src/elva_cli/registry.py +0 -5
- elva_cli-0.0.4/src/elva_cli/settings/__init__.py +0 -5
- elva_cli-0.0.4/src/elva_cli/settings/paths.py +0 -16
- elva_cli-0.0.4/src/elva_cli/ui/prompts.py +0 -9
- elva_cli-0.0.4/src/elva_cli/ui/renderables/__init__.py +0 -4
- {elva_cli-0.0.4 → elva_cli-0.2.0}/.gitignore +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/__main__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/auth/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/commands/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/core/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/core/api/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/core/services/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/core/spec/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/errors.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/logging.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/telemetry.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/ui/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/ui/views/__init__.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/src/elva_cli/update.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/tests/cli/test_cli_exit_codes.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/tests/unit/test_error_boundary.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/tests/unit/test_errors.py +0 -0
- {elva_cli-0.0.4 → elva_cli-0.2.0}/tests/unit/test_exit_codes.py +0 -0
elva_cli-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: elva-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Elva - CLI for Theneo Elva
|
|
5
|
+
Project-URL: Homepage, https://getelva.ai
|
|
6
|
+
Project-URL: Source, https://github.com/Theneo-Inc/theneo-elva-cli
|
|
7
|
+
Project-URL: Issues, https://github.com/Theneo-Inc/theneo-elva-cli/issues
|
|
8
|
+
Author-email: Theneo <support@theneo.io>
|
|
9
|
+
Keywords: api,cli,documentation,elva,openapi,theneo
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: platformdirs>=4.2
|
|
20
|
+
Requires-Dist: pydantic>=2.7
|
|
21
|
+
Requires-Dist: typer<1.0,>=0.15
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.2; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Elva CLI
|
|
29
|
+
|
|
30
|
+
Manage your [Elva](https://getelva.ai) API projects from the terminal: import specs,
|
|
31
|
+
inspect collections, and generate MCP servers without opening a browser.
|
|
32
|
+
|
|
33
|
+
> **Early alpha.** The command surface is still taking shape. This release ships
|
|
34
|
+
> `--version` and `--help` only; the first working commands land in `0.1.0`.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Requires Python 3.11 or newer.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install elva-cli
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Or with [pipx](https://pipx.pypa.io/), if you already use it:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pipx install elva-cli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Either way, `elva` is then available from any directory:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
elva --version
|
|
54
|
+
elva --help
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
To try it without installing anything:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
uvx --from elva-cli elva --version
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Don't have `uv`? It is a single command and no prerequisites:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
curl -fsSL https://astral.sh/uv/install.sh | sh # macOS, Linux
|
|
67
|
+
powershell -c "irm https://astral.sh/uv/install.ps1|iex" # Windows
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Upgrade
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
uv tool upgrade elva-cli # or: pipx upgrade elva-cli
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
Settings can come from several places. Highest priority wins:
|
|
79
|
+
|
|
80
|
+
1. Command flags: `--workspace`, `--collection`, `--profile`
|
|
81
|
+
2. Environment: `ELVA_WORKSPACE`, `ELVA_COLLECTION`, `ELVA_PROFILE`, `ELVA_TIMEOUT`
|
|
82
|
+
3. `elva.json` in your project
|
|
83
|
+
4. The selected profile in your user config
|
|
84
|
+
5. Your user config
|
|
85
|
+
6. Built in defaults
|
|
86
|
+
|
|
87
|
+
### Project file
|
|
88
|
+
|
|
89
|
+
Commit an `elva.json` next to your spec and stop repeating flags:
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"workspace": "payments-team",
|
|
94
|
+
"collection": "payments-api"
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
It is found by walking up from the current directory to the repo root, so it works
|
|
99
|
+
from any subfolder. Keep secrets out of it, it is meant to be committed.
|
|
100
|
+
|
|
101
|
+
### User config and profiles
|
|
102
|
+
|
|
103
|
+
| Platform | Location |
|
|
104
|
+
|---|---|
|
|
105
|
+
| Linux | `~/.config/elva/config.json` |
|
|
106
|
+
| macOS | `~/Library/Application Support/elva/config.json` |
|
|
107
|
+
| Windows | `%LOCALAPPDATA%\elva\config.json` |
|
|
108
|
+
|
|
109
|
+
A profile is a named set of defaults. Useful when you work across more than one
|
|
110
|
+
workspace and do not want a project file for each:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"profiles": {
|
|
115
|
+
"work": { "workspace": "work-team", "collection": "work-api" },
|
|
116
|
+
"side": { "workspace": "side-team" }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
elva --profile work collection list
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
A project file beats a profile, so a repo with its own `elva.json` always wins over
|
|
126
|
+
whichever profile you have selected.
|
|
127
|
+
|
|
128
|
+
### Seeing what was resolved
|
|
129
|
+
|
|
130
|
+
When something targets the wrong place, these two answer it:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
elva config path # which files were read, and whether they exist
|
|
134
|
+
elva config list # each value, and which layer set it
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
$ elva --profile work config list
|
|
139
|
+
collection work-api profile:work
|
|
140
|
+
profile work flag
|
|
141
|
+
timeout 30.0 default
|
|
142
|
+
workspace work-team profile:work
|
|
143
|
+
|
|
144
|
+
profiles: side, work
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Every command also takes `--json` for scripting:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
$ elva --profile work --json config list
|
|
151
|
+
{
|
|
152
|
+
"profile": "work",
|
|
153
|
+
"settings": [
|
|
154
|
+
{ "key": "collection", "value": "work-api", "origin": "profile:work" },
|
|
155
|
+
{ "key": "workspace", "value": "work-team", "origin": "profile:work" }
|
|
156
|
+
],
|
|
157
|
+
"profiles": ["side", "work"]
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Data goes to stdout and everything else to stderr, so `elva --json ... | jq` is always
|
|
162
|
+
clean. `--quiet` drops hints and warnings but keeps data and errors. `--no-color` and
|
|
163
|
+
`NO_COLOR` turn off styling.
|
|
164
|
+
|
|
165
|
+
## Requirements
|
|
166
|
+
|
|
167
|
+
- Python 3.11 or newer (bundled automatically if you install via `uv tool`)
|
|
168
|
+
- An [Elva](https://getelva.ai) account
|
|
169
|
+
|
|
170
|
+
## Links
|
|
171
|
+
|
|
172
|
+
- [Elva](https://getelva.ai)
|
|
173
|
+
- [Issues](https://github.com/Theneo-Inc/theneo-elva-cli/issues)
|
|
174
|
+
- [Exit codes](docs/exit-codes.md), for scripting and CI
|
|
175
|
+
- [Contributing](CONTRIBUTING.md)
|
elva_cli-0.2.0/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# Elva CLI
|
|
2
|
+
|
|
3
|
+
Manage your [Elva](https://getelva.ai) API projects from the terminal: import specs,
|
|
4
|
+
inspect collections, and generate MCP servers without opening a browser.
|
|
5
|
+
|
|
6
|
+
> **Early alpha.** The command surface is still taking shape. This release ships
|
|
7
|
+
> `--version` and `--help` only; the first working commands land in `0.1.0`.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Requires Python 3.11 or newer.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
uv tool install elva-cli
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Or with [pipx](https://pipx.pypa.io/), if you already use it:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pipx install elva-cli
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Either way, `elva` is then available from any directory:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
elva --version
|
|
27
|
+
elva --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
To try it without installing anything:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uvx --from elva-cli elva --version
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Don't have `uv`? It is a single command and no prerequisites:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
curl -fsSL https://astral.sh/uv/install.sh | sh # macOS, Linux
|
|
40
|
+
powershell -c "irm https://astral.sh/uv/install.ps1|iex" # Windows
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Upgrade
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv tool upgrade elva-cli # or: pipx upgrade elva-cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Settings can come from several places. Highest priority wins:
|
|
52
|
+
|
|
53
|
+
1. Command flags: `--workspace`, `--collection`, `--profile`
|
|
54
|
+
2. Environment: `ELVA_WORKSPACE`, `ELVA_COLLECTION`, `ELVA_PROFILE`, `ELVA_TIMEOUT`
|
|
55
|
+
3. `elva.json` in your project
|
|
56
|
+
4. The selected profile in your user config
|
|
57
|
+
5. Your user config
|
|
58
|
+
6. Built in defaults
|
|
59
|
+
|
|
60
|
+
### Project file
|
|
61
|
+
|
|
62
|
+
Commit an `elva.json` next to your spec and stop repeating flags:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"workspace": "payments-team",
|
|
67
|
+
"collection": "payments-api"
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It is found by walking up from the current directory to the repo root, so it works
|
|
72
|
+
from any subfolder. Keep secrets out of it, it is meant to be committed.
|
|
73
|
+
|
|
74
|
+
### User config and profiles
|
|
75
|
+
|
|
76
|
+
| Platform | Location |
|
|
77
|
+
|---|---|
|
|
78
|
+
| Linux | `~/.config/elva/config.json` |
|
|
79
|
+
| macOS | `~/Library/Application Support/elva/config.json` |
|
|
80
|
+
| Windows | `%LOCALAPPDATA%\elva\config.json` |
|
|
81
|
+
|
|
82
|
+
A profile is a named set of defaults. Useful when you work across more than one
|
|
83
|
+
workspace and do not want a project file for each:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"profiles": {
|
|
88
|
+
"work": { "workspace": "work-team", "collection": "work-api" },
|
|
89
|
+
"side": { "workspace": "side-team" }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
elva --profile work collection list
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
A project file beats a profile, so a repo with its own `elva.json` always wins over
|
|
99
|
+
whichever profile you have selected.
|
|
100
|
+
|
|
101
|
+
### Seeing what was resolved
|
|
102
|
+
|
|
103
|
+
When something targets the wrong place, these two answer it:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
elva config path # which files were read, and whether they exist
|
|
107
|
+
elva config list # each value, and which layer set it
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
$ elva --profile work config list
|
|
112
|
+
collection work-api profile:work
|
|
113
|
+
profile work flag
|
|
114
|
+
timeout 30.0 default
|
|
115
|
+
workspace work-team profile:work
|
|
116
|
+
|
|
117
|
+
profiles: side, work
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Every command also takes `--json` for scripting:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
$ elva --profile work --json config list
|
|
124
|
+
{
|
|
125
|
+
"profile": "work",
|
|
126
|
+
"settings": [
|
|
127
|
+
{ "key": "collection", "value": "work-api", "origin": "profile:work" },
|
|
128
|
+
{ "key": "workspace", "value": "work-team", "origin": "profile:work" }
|
|
129
|
+
],
|
|
130
|
+
"profiles": ["side", "work"]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Data goes to stdout and everything else to stderr, so `elva --json ... | jq` is always
|
|
135
|
+
clean. `--quiet` drops hints and warnings but keeps data and errors. `--no-color` and
|
|
136
|
+
`NO_COLOR` turn off styling.
|
|
137
|
+
|
|
138
|
+
## Requirements
|
|
139
|
+
|
|
140
|
+
- Python 3.11 or newer (bundled automatically if you install via `uv tool`)
|
|
141
|
+
- An [Elva](https://getelva.ai) account
|
|
142
|
+
|
|
143
|
+
## Links
|
|
144
|
+
|
|
145
|
+
- [Elva](https://getelva.ai)
|
|
146
|
+
- [Issues](https://github.com/Theneo-Inc/theneo-elva-cli/issues)
|
|
147
|
+
- [Exit codes](docs/exit-codes.md), for scripting and CI
|
|
148
|
+
- [Contributing](CONTRIBUTING.md)
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.0
|
|
22
|
-
__version_tuple__ = version_tuple = (0,
|
|
21
|
+
__version__ = version = '0.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from elva_cli.context import get_ctx
|
|
6
|
+
|
|
7
|
+
app = typer.Typer(name="config", help="Inspect resolved configuration.", no_args_is_help=True)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@app.command("path")
|
|
11
|
+
def path(click_ctx: typer.Context) -> None:
|
|
12
|
+
"""Show every file the CLI reads configuration from."""
|
|
13
|
+
from elva_cli.core.services.config import describe_paths
|
|
14
|
+
|
|
15
|
+
ctx = get_ctx(click_ctx)
|
|
16
|
+
ctx.out.result(describe_paths(ctx.resolution))
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@app.command("list")
|
|
20
|
+
def list_(click_ctx: typer.Context) -> None:
|
|
21
|
+
"""Show each setting, its value, and which layer set it."""
|
|
22
|
+
from elva_cli.core.services.config import describe_values
|
|
23
|
+
|
|
24
|
+
ctx = get_ctx(click_ctx)
|
|
25
|
+
ctx.out.result(describe_values(ctx.resolution))
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""The Ctx object, built once in the root callback and passed to every command.
|
|
2
|
+
|
|
3
|
+
Commands read settings from here instead of touching the environment or the
|
|
4
|
+
filesystem themselves, so configuration is resolved in exactly one place.
|
|
5
|
+
|
|
6
|
+
Resolution is a cached_property because it pulls in pydantic. `elva --version`
|
|
7
|
+
must not pay for that.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from functools import cached_property
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from elva_cli.errors import ElvaError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Mapping
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
import typer
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
|
|
26
|
+
from elva_cli.settings.loader import Resolution
|
|
27
|
+
from elva_cli.settings.models import Settings
|
|
28
|
+
from elva_cli.ui.output import Output
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class GlobalOptions:
|
|
33
|
+
profile: str | None = None
|
|
34
|
+
base_url: str | None = None
|
|
35
|
+
workspace: str | None = None
|
|
36
|
+
collection: str | None = None
|
|
37
|
+
json_output: bool = False
|
|
38
|
+
quiet: bool = False
|
|
39
|
+
color: bool | None = None
|
|
40
|
+
assume_yes: bool = False
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
CI_VARS = (
|
|
44
|
+
"CI",
|
|
45
|
+
"GITHUB_ACTIONS",
|
|
46
|
+
"GITLAB_CI",
|
|
47
|
+
"CIRCLECI",
|
|
48
|
+
"BUILDKITE",
|
|
49
|
+
"JENKINS_URL",
|
|
50
|
+
"TF_BUILD",
|
|
51
|
+
"TEAMCITY_VERSION",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
_FALSEY = frozenset({"", "0", "false", "no", "off"})
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class Ctx:
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
options: GlobalOptions,
|
|
61
|
+
*,
|
|
62
|
+
cwd: Path,
|
|
63
|
+
env: Mapping[str, str],
|
|
64
|
+
tty: bool | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
self.options = options
|
|
67
|
+
self.cwd = cwd
|
|
68
|
+
self.env = env
|
|
69
|
+
self._tty = tty
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def assume_yes(self) -> bool:
|
|
73
|
+
return self.options.assume_yes
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def is_ci(self) -> bool:
|
|
77
|
+
return any(self.env.get(var, "").strip().lower() not in _FALSEY for var in CI_VARS)
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_tty(self) -> bool:
|
|
81
|
+
"""Both streams, because a prompt needs to read and to be seen."""
|
|
82
|
+
if self._tty is not None:
|
|
83
|
+
return self._tty
|
|
84
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def interactive(self) -> bool:
|
|
88
|
+
"""Whether it is safe to ask the user a question.
|
|
89
|
+
|
|
90
|
+
The single place this is decided. --json means a machine is reading, so a
|
|
91
|
+
prompt would corrupt the output as well as hang. --quiet is not included:
|
|
92
|
+
it lowers the noise floor, it does not say nobody is there.
|
|
93
|
+
"""
|
|
94
|
+
if self.options.json_output or self.is_ci:
|
|
95
|
+
return False
|
|
96
|
+
return self.is_tty
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def color(self) -> bool | None:
|
|
100
|
+
"""NO_COLOR wins over everything except an explicit --color."""
|
|
101
|
+
if self.options.color is None and self.env.get("NO_COLOR"):
|
|
102
|
+
return False
|
|
103
|
+
return self.options.color
|
|
104
|
+
|
|
105
|
+
@cached_property
|
|
106
|
+
def consoles(self) -> tuple[Console, Console]:
|
|
107
|
+
from elva_cli.ui.console import build_consoles
|
|
108
|
+
|
|
109
|
+
return build_consoles(color=self.color, quiet=self.options.quiet)
|
|
110
|
+
|
|
111
|
+
@cached_property
|
|
112
|
+
def out(self) -> Output:
|
|
113
|
+
from elva_cli.ui.output import Output
|
|
114
|
+
|
|
115
|
+
stdout, stderr = self.consoles
|
|
116
|
+
return Output(
|
|
117
|
+
stdout=stdout,
|
|
118
|
+
stderr=stderr,
|
|
119
|
+
json_mode=self.options.json_output,
|
|
120
|
+
quiet=self.options.quiet,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
@cached_property
|
|
124
|
+
def resolution(self) -> Resolution:
|
|
125
|
+
from elva_cli.settings.loader import resolve
|
|
126
|
+
|
|
127
|
+
return resolve(
|
|
128
|
+
overrides={
|
|
129
|
+
"profile": self.options.profile,
|
|
130
|
+
"base_url": self.options.base_url,
|
|
131
|
+
"workspace": self.options.workspace,
|
|
132
|
+
"collection": self.options.collection,
|
|
133
|
+
},
|
|
134
|
+
env=self.env,
|
|
135
|
+
cwd=self.cwd,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
@property
|
|
139
|
+
def settings(self) -> Settings:
|
|
140
|
+
return self.resolution.settings
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def get_ctx(click_ctx: typer.Context) -> Ctx:
|
|
144
|
+
ctx = click_ctx.obj
|
|
145
|
+
if not isinstance(ctx, Ctx):
|
|
146
|
+
msg = "no Ctx on the context; the root callback did not run"
|
|
147
|
+
raise ElvaError(msg)
|
|
148
|
+
return ctx
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Use cases for inspecting configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from elva_cli.settings import paths
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from elva_cli.settings.loader import Resolution
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class ConfigFileInfo:
|
|
16
|
+
kind: str
|
|
17
|
+
path: str
|
|
18
|
+
exists: bool
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class ConfigPaths:
|
|
23
|
+
config_dir: str
|
|
24
|
+
cache_dir: str
|
|
25
|
+
files: list[ConfigFileInfo] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class SettingValue:
|
|
30
|
+
key: str
|
|
31
|
+
value: Any
|
|
32
|
+
origin: str
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class ConfigValues:
|
|
37
|
+
profile: str
|
|
38
|
+
settings: list[SettingValue] = field(default_factory=list)
|
|
39
|
+
profiles: list[str] = field(default_factory=list)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def describe_paths(resolution: Resolution) -> ConfigPaths:
|
|
43
|
+
"""Report every location the CLI consulted, whether or not it existed."""
|
|
44
|
+
return ConfigPaths(
|
|
45
|
+
config_dir=str(paths.config_dir()),
|
|
46
|
+
cache_dir=str(paths.cache_dir()),
|
|
47
|
+
files=[
|
|
48
|
+
ConfigFileInfo(kind=f.kind, path=str(f.path), exists=f.exists) for f in resolution.files
|
|
49
|
+
],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def describe_values(resolution: Resolution) -> ConfigValues:
|
|
54
|
+
"""Report each resolved setting alongside the layer that set it."""
|
|
55
|
+
settings = resolution.settings
|
|
56
|
+
return ConfigValues(
|
|
57
|
+
profile=settings.profile,
|
|
58
|
+
settings=[
|
|
59
|
+
SettingValue(
|
|
60
|
+
key=key,
|
|
61
|
+
value=getattr(settings, key),
|
|
62
|
+
origin=resolution.origins[key],
|
|
63
|
+
)
|
|
64
|
+
for key in sorted(type(settings).model_fields)
|
|
65
|
+
],
|
|
66
|
+
profiles=list(resolution.profiles),
|
|
67
|
+
)
|
|
@@ -5,16 +5,17 @@ from __future__ import annotations
|
|
|
5
5
|
import os
|
|
6
6
|
import platform
|
|
7
7
|
import sys
|
|
8
|
-
from
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Protocol, TypeGuard
|
|
9
10
|
|
|
10
11
|
import typer
|
|
11
12
|
|
|
13
|
+
from elva_cli.context import Ctx, GlobalOptions
|
|
12
14
|
from elva_cli.errors import ElvaError, ExitCode
|
|
13
|
-
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from pathlib import Path
|
|
15
|
+
from elva_cli.registry import LazyGroup
|
|
16
16
|
|
|
17
17
|
app = typer.Typer(
|
|
18
|
+
cls=LazyGroup,
|
|
18
19
|
name="elva",
|
|
19
20
|
help="Elva - CLI for Theneo Elva.",
|
|
20
21
|
no_args_is_help=True,
|
|
@@ -35,6 +36,31 @@ def _version_callback(value: bool) -> None:
|
|
|
35
36
|
|
|
36
37
|
@app.callback()
|
|
37
38
|
def root(
|
|
39
|
+
click_ctx: typer.Context,
|
|
40
|
+
profile: str | None = typer.Option(
|
|
41
|
+
None, "--profile", envvar="ELVA_PROFILE", help="Named set of defaults from your config."
|
|
42
|
+
),
|
|
43
|
+
# Internal escape hatch for Theneo development and CI against staging. Users
|
|
44
|
+
# only ever have prod, so it stays out of --help and out of the README.
|
|
45
|
+
base_url: str | None = typer.Option(None, "--base-url", envvar="ELVA_BASE_URL", hidden=True),
|
|
46
|
+
workspace: str | None = typer.Option(
|
|
47
|
+
None, "--workspace", "-w", envvar="ELVA_WORKSPACE", help="Workspace to act on."
|
|
48
|
+
),
|
|
49
|
+
collection: str | None = typer.Option(
|
|
50
|
+
None, "--collection", "-c", envvar="ELVA_COLLECTION", help="Collection to act on."
|
|
51
|
+
),
|
|
52
|
+
json_output: bool = typer.Option(
|
|
53
|
+
False, "--json", help="Emit machine readable JSON instead of formatted output."
|
|
54
|
+
),
|
|
55
|
+
quiet: bool = typer.Option(
|
|
56
|
+
False, "--quiet", "-q", help="Suppress hints and warnings. Data and errors still print."
|
|
57
|
+
),
|
|
58
|
+
assume_yes: bool = typer.Option(
|
|
59
|
+
False, "--yes", "-y", help="Answer every confirmation with yes. Required in CI."
|
|
60
|
+
),
|
|
61
|
+
color: bool | None = typer.Option(
|
|
62
|
+
None, "--color/--no-color", help="Force or disable colour. Honours NO_COLOR."
|
|
63
|
+
),
|
|
38
64
|
version: bool = typer.Option(
|
|
39
65
|
False,
|
|
40
66
|
"--version",
|
|
@@ -44,14 +70,31 @@ def root(
|
|
|
44
70
|
help="Show the current build version.",
|
|
45
71
|
),
|
|
46
72
|
) -> None:
|
|
47
|
-
|
|
73
|
+
click_ctx.obj = Ctx(
|
|
74
|
+
GlobalOptions(
|
|
75
|
+
profile=profile,
|
|
76
|
+
base_url=base_url,
|
|
77
|
+
workspace=workspace,
|
|
78
|
+
collection=collection,
|
|
79
|
+
json_output=json_output,
|
|
80
|
+
quiet=quiet,
|
|
81
|
+
color=color,
|
|
82
|
+
assume_yes=assume_yes,
|
|
83
|
+
),
|
|
84
|
+
cwd=Path.cwd(),
|
|
85
|
+
env=os.environ,
|
|
86
|
+
)
|
|
48
87
|
|
|
49
88
|
|
|
50
89
|
def report(error: ElvaError) -> None:
|
|
51
|
-
"""Render a user-facing error to stderr
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
90
|
+
"""Render a user-facing error to stderr.
|
|
91
|
+
|
|
92
|
+
Goes through ui/ so errors look the same wherever they come from, but does not
|
|
93
|
+
need a Ctx: the boundary has to work when building the Ctx is what failed.
|
|
94
|
+
"""
|
|
95
|
+
from elva_cli.ui.output import report_error
|
|
96
|
+
|
|
97
|
+
report_error(error, color=False if os.environ.get("NO_COLOR") else None)
|
|
55
98
|
|
|
56
99
|
|
|
57
100
|
def write_crash(exc: BaseException) -> Path | None:
|