impose-cli 0.4.1__tar.gz → 1.0.0rc0__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.
- impose_cli-1.0.0rc0/.changeset/README.md +9 -0
- impose_cli-1.0.0rc0/.gitignore +19 -0
- impose_cli-1.0.0rc0/PKG-INFO +551 -0
- impose_cli-1.0.0rc0/README.md +540 -0
- impose_cli-1.0.0rc0/examples/__init__.py +1 -0
- impose_cli-1.0.0rc0/examples/admin_tool/README.md +99 -0
- impose_cli-1.0.0rc0/examples/admin_tool/__init__.py +1 -0
- impose_cli-1.0.0rc0/examples/admin_tool/admin_tool/__init__.py +1 -0
- impose_cli-1.0.0rc0/examples/admin_tool/admin_tool/application.py +33 -0
- impose_cli-1.0.0rc0/examples/admin_tool/admin_tool/data.py +37 -0
- impose_cli-1.0.0rc0/examples/admin_tool/admin_tool/middleware.py +37 -0
- impose_cli-1.0.0rc0/examples/admin_tool/admin_tool/storage.py +22 -0
- impose_cli-1.0.0rc0/examples/admin_tool/admin_tool/users.py +39 -0
- impose_cli-1.0.0rc0/examples/admin_tool/api.py +8 -0
- impose_cli-1.0.0rc0/examples/admin_tool/cli.py +11 -0
- impose_cli-1.0.0rc0/integration/pytest.ini +5 -0
- impose_cli-1.0.0rc0/integration/test_admin_tool.py +136 -0
- impose_cli-1.0.0rc0/pyproject.toml +89 -0
- impose_cli-1.0.0rc0/src/impose/__init__.py +10 -0
- impose_cli-1.0.0rc0/src/impose/__main__.py +31 -0
- impose_cli-1.0.0rc0/src/impose/api.py +199 -0
- impose_cli-1.0.0rc0/src/impose/application.py +110 -0
- impose_cli-1.0.0rc0/src/impose/cli/__init__.py +7 -0
- impose_cli-1.0.0rc0/src/impose/cli/arguments.py +180 -0
- impose_cli-1.0.0rc0/src/impose/cli/builder.py +132 -0
- impose_cli-1.0.0rc0/src/impose/cli/confirmation.py +86 -0
- impose_cli-1.0.0rc0/src/impose/cli/formatting.py +75 -0
- impose_cli-1.0.0rc0/src/impose/commands.py +55 -0
- impose_cli-1.0.0rc0/src/impose/decorator.py +77 -0
- impose_cli-1.0.0rc0/src/impose/discovery.py +105 -0
- impose_cli-1.0.0rc0/src/impose/docstrings.py +34 -0
- impose_cli-1.0.0rc0/src/impose/middleware.py +68 -0
- impose_cli-1.0.0rc0/src/impose/values.py +115 -0
- impose_cli-1.0.0rc0/src/tests/helpers.py +22 -0
- impose_cli-1.0.0rc0/src/tests/test_api.py +382 -0
- impose_cli-1.0.0rc0/src/tests/test_api_fuzz.py +99 -0
- impose_cli-1.0.0rc0/src/tests/test_cli_arguments.py +90 -0
- impose_cli-1.0.0rc0/src/tests/test_cli_confirmation.py +108 -0
- impose_cli-1.0.0rc0/src/tests/test_cli_runtime.py +65 -0
- impose_cli-1.0.0rc0/src/tests/test_commands.py +37 -0
- impose_cli-1.0.0rc0/src/tests/test_decorator.py +52 -0
- impose_cli-1.0.0rc0/src/tests/test_discovery.py +132 -0
- impose_cli-1.0.0rc0/src/tests/test_formatting.py +40 -0
- impose_cli-1.0.0rc0/src/tests/test_integration.py +96 -0
- impose_cli-1.0.0rc0/src/tests/test_main.py +27 -0
- impose_cli-1.0.0rc0/src/tests/test_values.py +60 -0
- impose_cli-1.0.0rc0/unit/pytest.ini +9 -0
- impose_cli-1.0.0rc0/unit/test_unit.py +33 -0
- impose-cli-0.4.1/LICENSE +0 -21
- impose-cli-0.4.1/PKG-INFO +0 -11
- impose-cli-0.4.1/README.md +0 -25
- impose-cli-0.4.1/impose_cli/__init__.py +0 -4
- impose-cli-0.4.1/impose_cli/_utils.py +0 -275
- impose-cli-0.4.1/impose_cli/decorators.py +0 -15
- impose-cli-0.4.1/impose_cli/errors.py +0 -5
- impose-cli-0.4.1/impose_cli/impose_cli.py +0 -19
- impose-cli-0.4.1/impose_cli.egg-info/PKG-INFO +0 -11
- impose-cli-0.4.1/impose_cli.egg-info/SOURCES.txt +0 -15
- impose-cli-0.4.1/impose_cli.egg-info/dependency_links.txt +0 -1
- impose-cli-0.4.1/impose_cli.egg-info/requires.txt +0 -3
- impose-cli-0.4.1/impose_cli.egg-info/top_level.txt +0 -2
- impose-cli-0.4.1/setup.cfg +0 -4
- impose-cli-0.4.1/setup.py +0 -19
- impose-cli-0.4.1/tests/__init__.py +0 -0
- impose-cli-0.4.1/tests/simple.py +0 -23
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
impose-cli.iml
|
|
2
|
+
venv/
|
|
3
|
+
.idea/
|
|
4
|
+
.env
|
|
5
|
+
.direnv/
|
|
6
|
+
.pytest_cache/
|
|
7
|
+
.coverage
|
|
8
|
+
.coverage.*
|
|
9
|
+
coverage.xml
|
|
10
|
+
coverage.json
|
|
11
|
+
htmlcov/
|
|
12
|
+
.venv/
|
|
13
|
+
node_modules/
|
|
14
|
+
dist/
|
|
15
|
+
*.egg-info/
|
|
16
|
+
__pycache__/
|
|
17
|
+
**/__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*.iml
|
|
@@ -0,0 +1,551 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: impose-cli
|
|
3
|
+
Version: 1.0.0rc0
|
|
4
|
+
Summary: Command line tools for impose.
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: docstring-parser>=0.18
|
|
7
|
+
Requires-Dist: pydantic>=2.0
|
|
8
|
+
Provides-Extra: api
|
|
9
|
+
Requires-Dist: fastapi>=0.100; extra == 'api'
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# impose-cli
|
|
13
|
+
|
|
14
|
+
impose-cli is the fastest and least disruptive way to turn your project, or a subset of your project, into a CLI or API.
|
|
15
|
+
|
|
16
|
+
Create an `ImposeApplication()` and point it at a module. Impose will iterate through that module and its submodules, then create a CLI and optionally a FastAPI application that can be mounted into another FastAPI application.
|
|
17
|
+
|
|
18
|
+
Every function decorated with `@impose` becomes a command while remaining a normal Python function. Add `impose_api_method` when the function should also become an API endpoint.
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from impose import ImposeApplication, impose
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@impose(impose_cs="elbv2")
|
|
25
|
+
def list_load_balancers(region: str) -> list[str]:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
app = ImposeApplication()
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Disruption Free
|
|
33
|
+
|
|
34
|
+
The primary goal of Impose is to be disruption free. Adding `@impose` to a function does not change how the function is called from Python code, does not require the function to inherit from a framework type, and does not prevent the function from being imported and reused normally.
|
|
35
|
+
|
|
36
|
+
The decorator is a passthrough:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
@impose(impose_cs="elbv2")
|
|
40
|
+
def list_load_balancers(region: str) -> list[str]:
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
load_balancers = list_load_balancers("us-east-1")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Impose-specific decorator options are namespaced with the `impose_` prefix, such as `impose_cs`. This keeps Impose configuration separate from the function's own parameters and limits the chance of keyword argument interference as the function evolves.
|
|
48
|
+
|
|
49
|
+
## Command Sets
|
|
50
|
+
|
|
51
|
+
`@impose` accepts an `impose_cs` keyword argument, short for `impose_command_set`:
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
@impose(impose_cs="elbv2")
|
|
55
|
+
def describe_target_groups() -> list[str]:
|
|
56
|
+
...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
All functions under the same command set automatically become subcommands under the same CLI group. Functions that also set `impose_api_method` become endpoints under the same API router path.
|
|
60
|
+
|
|
61
|
+
Command descriptions and per-argument help come from the function docstring, keeping the decorator focused on command registration.
|
|
62
|
+
|
|
63
|
+
### Example: Explicit Command Sets
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# cloud/elbv2.py
|
|
67
|
+
from impose import impose
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@impose(impose_cs="elbv2")
|
|
71
|
+
def list_load_balancers(region: str) -> list[str]:
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@impose(impose_cs="elbv2")
|
|
76
|
+
def describe_target_groups(load_balancer_arn: str) -> list[str]:
|
|
77
|
+
...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# cli.py
|
|
82
|
+
import cloud.elbv2
|
|
83
|
+
from impose import ImposeApplication
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
application = ImposeApplication(modules=[cloud.elbv2])
|
|
87
|
+
cli = application.cli()
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The functions above become subcommands under the same `elbv2` group:
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
impose elbv2 list-load-balancers us-east-1
|
|
94
|
+
impose elbv2 describe-target-groups arn:aws:...
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Project Structure
|
|
98
|
+
|
|
99
|
+
Impose can optionally use the actual project structure to dynamically create commands and subcommands. For example, functions under an `elbv2` folder can become subcommands of the `elbv2` command.
|
|
100
|
+
|
|
101
|
+
### Example: Dynamic Project Structure
|
|
102
|
+
|
|
103
|
+
Given a project like this:
|
|
104
|
+
|
|
105
|
+
```text
|
|
106
|
+
cloud_tools/
|
|
107
|
+
__init__.py
|
|
108
|
+
aws/
|
|
109
|
+
__init__.py
|
|
110
|
+
elbv2.py
|
|
111
|
+
rds.py
|
|
112
|
+
s3.py
|
|
113
|
+
github/
|
|
114
|
+
__init__.py
|
|
115
|
+
repos.py
|
|
116
|
+
cli.py
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# cloud_tools/aws/elbv2.py
|
|
121
|
+
from impose import impose
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@impose
|
|
125
|
+
def list_load_balancers(region: str) -> list[str]:
|
|
126
|
+
...
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@impose
|
|
130
|
+
def describe_target_group(target_group_arn: str) -> dict[str, str]:
|
|
131
|
+
...
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
# cloud_tools/aws/s3.py
|
|
136
|
+
from impose import impose
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@impose
|
|
140
|
+
def list_buckets(profile: str | None = None) -> list[str]:
|
|
141
|
+
...
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
# cloud_tools/aws/rds.py
|
|
146
|
+
from impose import impose
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@impose
|
|
150
|
+
def reboot_instance(identifier: str, force_failover: bool = False) -> None:
|
|
151
|
+
...
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
# cloud_tools/github/repos.py
|
|
156
|
+
from impose import impose
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@impose
|
|
160
|
+
def archive_repo(owner: str, repo: str) -> None:
|
|
161
|
+
...
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Configure Impose to use the Python package structure as the command structure:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
# cloud_tools/cli.py
|
|
168
|
+
import cloud_tools
|
|
169
|
+
from impose import ImposeApplication
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
application = ImposeApplication(
|
|
173
|
+
root_module=cloud_tools,
|
|
174
|
+
use_project_structure=True,
|
|
175
|
+
modules_as_subcommands=True,
|
|
176
|
+
)
|
|
177
|
+
cli = application.cli()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`modules_as_subcommands` is a global setting that defaults to `True`. With the default behavior, Impose reads the modules below `cloud_tools`, finds functions decorated with `@impose`, and creates commands shaped like the package tree, including module filenames:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
impose aws elbv2 list-load-balancers us-east-1
|
|
184
|
+
impose aws elbv2 describe-target-group arn:aws:...
|
|
185
|
+
impose aws rds reboot-instance prod-db-1 --force-failover
|
|
186
|
+
impose aws s3 list-buckets --profile prod
|
|
187
|
+
impose github repos archive-repo example old-service
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
The generated command hierarchy mirrors the source layout:
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
cloud_tools/aws/elbv2.py:list_load_balancers
|
|
194
|
+
-> impose aws elbv2 list-load-balancers
|
|
195
|
+
|
|
196
|
+
cloud_tools/aws/elbv2.py:describe_target_group
|
|
197
|
+
-> impose aws elbv2 describe-target-group
|
|
198
|
+
|
|
199
|
+
cloud_tools/aws/rds.py:reboot_instance
|
|
200
|
+
-> impose aws rds reboot-instance
|
|
201
|
+
|
|
202
|
+
cloud_tools/aws/s3.py:list_buckets
|
|
203
|
+
-> impose aws s3 list-buckets
|
|
204
|
+
|
|
205
|
+
cloud_tools/github/repos.py:archive_repo
|
|
206
|
+
-> impose github repos archive-repo
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
If you only want folders/packages to create command groups, disable module-generated subcommands:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# cloud_tools/cli.py
|
|
213
|
+
import cloud_tools
|
|
214
|
+
from impose import ImposeApplication
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
application = ImposeApplication(
|
|
218
|
+
root_module=cloud_tools,
|
|
219
|
+
use_project_structure=True,
|
|
220
|
+
modules_as_subcommands=False,
|
|
221
|
+
)
|
|
222
|
+
cli = application.cli()
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
With `modules_as_subcommands=False`, the folder still creates the `aws` group, but `rds.py`, `s3.py`, and `elbv2.py` do not add another command level:
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
impose aws list-load-balancers us-east-1
|
|
229
|
+
impose aws describe-target-group arn:aws:...
|
|
230
|
+
impose aws reboot-instance prod-db-1 --force-failover
|
|
231
|
+
impose aws list-buckets --profile prod
|
|
232
|
+
impose github archive-repo example old-service
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## FastAPI
|
|
236
|
+
|
|
237
|
+
Impose can create a FastAPI application or router from decorated functions that explicitly opt into API exposure. The generated API can be mounted into another FastAPI application, which lets you expose project functionality without building a separate API layer by hand.
|
|
238
|
+
|
|
239
|
+
### Example: Mounting into FastAPI
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
# api.py
|
|
243
|
+
import cloud_tools
|
|
244
|
+
from fastapi import FastAPI
|
|
245
|
+
from impose import ImposeApplication
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
service = FastAPI()
|
|
249
|
+
|
|
250
|
+
impose_application = ImposeApplication(
|
|
251
|
+
root_module=cloud_tools,
|
|
252
|
+
use_project_structure=True,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
service.include_router(
|
|
256
|
+
impose_application.api_router(),
|
|
257
|
+
prefix="/internal/tools",
|
|
258
|
+
)
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Or create a standalone FastAPI app directly:
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
app = impose_application.api_app(title="Internal Tools")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
A decorated function such as:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
@impose(impose_cs="elbv2", impose_api_method="GET")
|
|
271
|
+
def list_load_balancers(region: str) -> list[str]:
|
|
272
|
+
...
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
can be exposed as an endpoint under the generated router:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
GET /internal/tools/elbv2/list-load-balancers?region=us-east-1
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
If a function is decorated with `@impose` but does not set `impose_api_method`, it remains available to the CLI but is skipped when `api_router()` or `api_app()` is built. Impose emits a warning for each skipped function so accidental API omissions are visible during app startup.
|
|
282
|
+
|
|
283
|
+
Supported API methods are `GET`, `PUT`, `POST`, `PATCH`, and `DELETE`. Any other `impose_api_method` value fails loudly when the API app or router is created.
|
|
284
|
+
|
|
285
|
+
### API Parameter Rules
|
|
286
|
+
|
|
287
|
+
`GET` endpoints read function parameters from query parameters:
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
@impose(impose_api_method="GET")
|
|
291
|
+
def list_users(tier: str, limit: int = 100) -> list[str]:
|
|
292
|
+
...
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
```text
|
|
296
|
+
GET /list-users?tier=paid&limit=25
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
`DELETE` endpoints read function parameters from path parameters. Impose appends the function parameters to the route path:
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
@impose(impose_api_method="DELETE")
|
|
303
|
+
def delete_user(user_id: str, hard: bool) -> str:
|
|
304
|
+
...
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```text
|
|
308
|
+
DELETE /delete-user/{user_id}/{hard}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
`PUT`, `POST`, and `PATCH` endpoints read every function parameter from the JSON request body:
|
|
312
|
+
|
|
313
|
+
```python
|
|
314
|
+
from pydantic import BaseModel
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class Region(BaseModel):
|
|
318
|
+
partition: str
|
|
319
|
+
name: str
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@impose(impose_api_method="POST")
|
|
323
|
+
def list_instances(region: Region, owner: str) -> list[str]:
|
|
324
|
+
...
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
The body nests Pydantic models under their parameter names:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"region": {
|
|
332
|
+
"partition": "aws",
|
|
333
|
+
"name": "us-east-1"
|
|
334
|
+
},
|
|
335
|
+
"owner": "platform"
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
`GET` and `DELETE` only support primitive-like arguments: `str`, `int`, `float`, `bool`, enums, literals of primitive values, and optional unions of those types. Pydantic models and other structured types are rejected for `GET` and `DELETE`; Impose raises an error during API app/router creation instead of creating an invalid route.
|
|
340
|
+
|
|
341
|
+
## Middleware
|
|
342
|
+
|
|
343
|
+
Impose supports FastAPI-style HTTP middleware on the whole generated API, on command sets, and on individual endpoints. Middleware runs for generated API routes only; CLI calls execute the Python function directly.
|
|
344
|
+
|
|
345
|
+
Middleware functions receive a request and `call_next`, matching FastAPI's `@app.middleware("http")` style:
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
from fastapi import HTTPException, Request, Response
|
|
349
|
+
|
|
350
|
+
from impose import impose
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
async def require_admin_scope(request: Request, call_next) -> Response:
|
|
354
|
+
scopes = set(request.headers.get("x-auth-user-scopes", "").split())
|
|
355
|
+
if "admin" not in scopes:
|
|
356
|
+
raise HTTPException(status_code=403, detail="Missing admin scope.")
|
|
357
|
+
return await call_next(request)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@impose(
|
|
361
|
+
impose_cs="deploy",
|
|
362
|
+
impose_api_method="POST",
|
|
363
|
+
impose_middleware=[require_admin_scope],
|
|
364
|
+
)
|
|
365
|
+
def restart_service(name: str, environment: str) -> str:
|
|
366
|
+
...
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Add middleware to every generated endpoint:
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from impose import ImposeApplication
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
application = ImposeApplication()
|
|
376
|
+
application.add_middleware(require_admin_scope)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
This works whether you call `application.api_app()` or include `application.api_router()` in another FastAPI app. In router mode, app-wide middleware means every generated Impose endpoint in that router; it does not become global middleware for unrelated routes in the parent app.
|
|
380
|
+
|
|
381
|
+
Add middleware to one command set:
|
|
382
|
+
|
|
383
|
+
```python
|
|
384
|
+
application.add_command_set_middleware(
|
|
385
|
+
"admin",
|
|
386
|
+
require_admin_scope,
|
|
387
|
+
)
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Every generated API endpoint in the `admin` command set will require the admin scope.
|
|
391
|
+
|
|
392
|
+
## Confirmation Prompts
|
|
393
|
+
|
|
394
|
+
Commands that need an explicit safety check can opt into confirmation:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
from impose import impose
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
@impose(impose_require_confirmation=True)
|
|
401
|
+
def delete_user(username: str, reason: str) -> None:
|
|
402
|
+
...
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
After all CLI arguments have been parsed, including any values collected through `-i` interactive mode, Impose prints an alarming confirmation message with a table of the exact function arguments. Long values are truncated instead of wrapped. The command only runs when the user types `yes`; typing `no` aborts it.
|
|
406
|
+
|
|
407
|
+
## Tests
|
|
408
|
+
|
|
409
|
+
Run unit tests with 100% coverage enforcement:
|
|
410
|
+
|
|
411
|
+
```sh
|
|
412
|
+
uv run pytest unit
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Run admin-tool integration tests without coverage enforcement:
|
|
416
|
+
|
|
417
|
+
```sh
|
|
418
|
+
uv run pytest integration
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
## Serialization
|
|
422
|
+
|
|
423
|
+
Custom serializers can be attached to types expected by imposed functions using Pydantic. This lets your CLI and API share the same typed interface while still accepting and returning rich project-specific objects.
|
|
424
|
+
|
|
425
|
+
### Example: Custom Serialization with Pydantic
|
|
426
|
+
|
|
427
|
+
```python
|
|
428
|
+
from pydantic import BaseModel, field_serializer
|
|
429
|
+
|
|
430
|
+
from impose import impose
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class Region(BaseModel):
|
|
434
|
+
partition: str
|
|
435
|
+
name: str
|
|
436
|
+
|
|
437
|
+
@field_serializer("name")
|
|
438
|
+
def serialize_name(self, value: str) -> str:
|
|
439
|
+
return value.lower()
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
@impose(impose_cs="ec2")
|
|
443
|
+
def list_instances(region: Region) -> list[str]:
|
|
444
|
+
...
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
Invoke it from the CLI by passing the Pydantic model as structured input:
|
|
448
|
+
|
|
449
|
+
```sh
|
|
450
|
+
impose ec2 list-instances '{"partition":"aws","name":"US-EAST-1"}'
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
The `field_serializer` normalizes the region name when the value is serialized, so downstream CLI output or API responses can return `us-east-1`.
|
|
454
|
+
|
|
455
|
+
If the same function is exposed through FastAPI, the generated endpoint can accept the same shape in the request body:
|
|
456
|
+
|
|
457
|
+
```sh
|
|
458
|
+
curl -X POST http://localhost:8000/ec2/list-instances \
|
|
459
|
+
-H 'content-type: application/json' \
|
|
460
|
+
-d '{"region":{"partition":"aws","name":"US-EAST-1"}}'
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
The same type can be used by the CLI parser and the generated API schema, so project-specific values do not need separate CLI and HTTP representations.
|
|
464
|
+
|
|
465
|
+
### Example: Rich Return Types
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
from pydantic import BaseModel
|
|
469
|
+
|
|
470
|
+
from impose import impose
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
class LoadBalancer(BaseModel):
|
|
474
|
+
name: str
|
|
475
|
+
arn: str
|
|
476
|
+
scheme: str
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
@impose(impose_cs="elbv2")
|
|
480
|
+
def get_load_balancer(name: str) -> LoadBalancer:
|
|
481
|
+
...
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
When exposed through the API, the Pydantic model becomes the response schema. When used from the CLI, Impose can serialize the result into a stable command output format.
|
|
485
|
+
|
|
486
|
+
## Interactive Mode
|
|
487
|
+
|
|
488
|
+
Impose supports a global `-i` mode that turns your impose command into an interactive command. Enums, Literals, and Booleans become interactive options that can be selected instead of typed manually.
|
|
489
|
+
|
|
490
|
+
### Example: Interactive Options
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
from enum import Enum
|
|
494
|
+
from typing import Literal
|
|
495
|
+
|
|
496
|
+
from impose import impose
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
class Environment(str, Enum):
|
|
500
|
+
dev = "dev"
|
|
501
|
+
staging = "staging"
|
|
502
|
+
prod = "prod"
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@impose(impose_cs="deploy")
|
|
506
|
+
def deploy_service(
|
|
507
|
+
service: str,
|
|
508
|
+
environment: Environment,
|
|
509
|
+
strategy: Literal["rolling", "blue-green"],
|
|
510
|
+
dry_run: bool = True,
|
|
511
|
+
) -> None:
|
|
512
|
+
...
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
Run the command normally:
|
|
516
|
+
|
|
517
|
+
```sh
|
|
518
|
+
impose deploy deploy-service \
|
|
519
|
+
billing \
|
|
520
|
+
prod \
|
|
521
|
+
rolling \
|
|
522
|
+
--dry-run
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Or use interactive mode:
|
|
526
|
+
|
|
527
|
+
```sh
|
|
528
|
+
impose -i deploy deploy-service
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
In interactive mode, Impose can prompt for `environment`, `strategy`, and `dry_run` using selectable choices.
|
|
532
|
+
|
|
533
|
+
## Development
|
|
534
|
+
|
|
535
|
+
Enter the development environment:
|
|
536
|
+
|
|
537
|
+
```sh
|
|
538
|
+
direnv allow
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Install the project environment:
|
|
542
|
+
|
|
543
|
+
```sh
|
|
544
|
+
uv sync
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
Run the CLI:
|
|
548
|
+
|
|
549
|
+
```sh
|
|
550
|
+
uv run impose --help
|
|
551
|
+
```
|