xtr-cli 4.0.2__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.
- xtr_cli-4.0.2/LICENSE +21 -0
- xtr_cli-4.0.2/PKG-INFO +307 -0
- xtr_cli-4.0.2/README.md +286 -0
- xtr_cli-4.0.2/pyproject.toml +161 -0
- xtr_cli-4.0.2/pyproject.toml.orig +162 -0
- xtr_cli-4.0.2/src/xtr_cli/__init__.py +79 -0
- xtr_cli-4.0.2/src/xtr_cli/__main__.py +28 -0
- xtr_cli-4.0.2/src/xtr_cli/add_on.py +52 -0
- xtr_cli-4.0.2/src/xtr_cli/add_on_catalog.py +167 -0
- xtr_cli-4.0.2/src/xtr_cli/add_on_choice.py +40 -0
- xtr_cli-4.0.2/src/xtr_cli/command/__init__.py +14 -0
- xtr_cli-4.0.2/src/xtr_cli/command/init_command.py +69 -0
- xtr_cli-4.0.2/src/xtr_cli/command/new_command.py +72 -0
- xtr_cli-4.0.2/src/xtr_cli/command/serve_command.py +114 -0
- xtr_cli-4.0.2/src/xtr_cli/command_support.py +189 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/__init__.py +34 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/ambiguous_web_entry_point_error.py +27 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/cli_error.py +14 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/directory_not_empty_error.py +35 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/invalid_project_name_error.py +33 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/invalid_template_error.py +28 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/project_not_found_error.py +28 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/step_failed_error.py +38 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/unknown_add_on_error.py +28 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/unknown_template_error.py +29 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/unsupported_python_error.py +28 -0
- xtr_cli-4.0.2/src/xtr_cli/exception/web_entry_point_not_found_error.py +31 -0
- xtr_cli-4.0.2/src/xtr_cli/process_runner_interface.py +29 -0
- xtr_cli-4.0.2/src/xtr_cli/project_name.py +55 -0
- xtr_cli-4.0.2/src/xtr_cli/project_questions.py +108 -0
- xtr_cli-4.0.2/src/xtr_cli/project_settings.py +89 -0
- xtr_cli-4.0.2/src/xtr_cli/py.typed +0 -0
- xtr_cli-4.0.2/src/xtr_cli/scaffold_plan.py +41 -0
- xtr_cli-4.0.2/src/xtr_cli/scaffolder.py +159 -0
- xtr_cli-4.0.2/src/xtr_cli/subprocess_runner.py +47 -0
- xtr_cli-4.0.2/src/xtr_cli/template.py +178 -0
- xtr_cli-4.0.2/src/xtr_cli/template_catalog.py +62 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/__init__.py +10 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/README.md.tmpl +44 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/__init__.py.tmpl +5 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/__main__.py.tmpl +23 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/command/__init__.py.tmpl +5 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/command/hello_command.py.tmpl +19 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/kernel.py.tmpl +34 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/py.typed.tmpl +0 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/gitignore.tmpl +14 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/pyproject.toml.tmpl +59 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/python-version.tmpl +1 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/tests/__init__.py.tmpl +0 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/tests/test_kernel.py.tmpl +9 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/cli/template.toml +34 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/README.md.tmpl +49 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/__init__.py.tmpl +5 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/__main__.py.tmpl +23 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/command/__init__.py.tmpl +5 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/command/hello_command.py.tmpl +19 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/config/__init__.py.tmpl +5 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/config/http_kernel.py.tmpl +19 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/kernel.py.tmpl +37 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/py.typed.tmpl +0 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/web/__init__.py.tmpl +5 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/web/__main__.py.tmpl +42 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/web/app.py.tmpl +23 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/web/routes.py.tmpl +15 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/gitignore.tmpl +14 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/pyproject.toml.tmpl +60 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/python-version.tmpl +1 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/tests/__init__.py.tmpl +0 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/tests/test_kernel.py.tmpl +9 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/tests/test_web.py.tmpl +28 -0
- xtr_cli-4.0.2/src/xtr_cli/templates/webapp/template.toml +45 -0
- xtr_cli-4.0.2/src/xtr_cli/web_entry_point.py +76 -0
- xtr_cli-4.0.2/src/xtr_cli/web_entry_point_finder.py +194 -0
xtr_cli-4.0.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xterr
|
|
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.
|
xtr_cli-4.0.2/PKG-INFO
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtr-cli
|
|
3
|
+
Version: 4.0.2
|
|
4
|
+
Summary: Scaffolds a new xtr application — a console application or a web application — in one command, and serves it.
|
|
5
|
+
Keywords: scaffold,new project,template,console,web,xtr-dependency-injection
|
|
6
|
+
Author: Xterr
|
|
7
|
+
Author-email: Xterr <me@xterr.dev>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
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: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: xtr-console>=4.0,<5
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
|
|
24
|
+
# xtr-cli
|
|
25
|
+
|
|
26
|
+
**A new xtr application — a console application or a web application — ready to run in one command, and served in another.**
|
|
27
|
+
|
|
28
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
29
|
+
<img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
|
|
30
|
+
<img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
31
|
+
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Why?
|
|
37
|
+
|
|
38
|
+
An application on the xtr packages starts the same way every time: a project manifest, a kernel,
|
|
39
|
+
an entry point that loads the environment and runs the console, a web entry point that serves
|
|
40
|
+
the application from the same kernel. Then each package's *Use in an application* steps — the
|
|
41
|
+
bundle list, the configuration files, the `.env` and ignore lines.
|
|
42
|
+
|
|
43
|
+
The packages already describe their own steps as recipes, which
|
|
44
|
+
[xtr-recipes](../xtr-recipes) applies. So this tool writes only what no recipe can — the
|
|
45
|
+
manifest, the kernel and the entry points — installs the packages with `uv`, and lets each
|
|
46
|
+
package's recipe configure itself. The project it leaves boots, passes its own tests, and is
|
|
47
|
+
already in the state `recipes:sync --check` expects.
|
|
48
|
+
|
|
49
|
+
## Install
|
|
50
|
+
|
|
51
|
+
Nothing to install: run it with `uvx`.
|
|
52
|
+
|
|
53
|
+
```sh
|
|
54
|
+
uvx xtr-cli new shop
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Or install it once, and call it `xtr`:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
uv tool install xtr-cli
|
|
61
|
+
xtr new shop
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/) on the `PATH`, and `git` unless
|
|
65
|
+
`--no-git` is given.
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```console
|
|
70
|
+
$ uvx xtr-cli new shop
|
|
71
|
+
• cli — A console application: a kernel, its commands, the layered .env files and logging.
|
|
72
|
+
• webapp — A web application: FastAPI served from the kernel, with the console beside it.
|
|
73
|
+
Template [cli/webapp] (cli): webapp
|
|
74
|
+
• orm — Database engines, sessions per unit of work and versioned migrations.
|
|
75
|
+
• messenger — A message bus: handlers, middleware, transports and consumers.
|
|
76
|
+
…
|
|
77
|
+
Add-ons, comma-separated (empty for none): orm, messenger
|
|
78
|
+
Database drivers for orm — sqlite, postgres, mysql, joined with + (sqlite): sqlite+postgres
|
|
79
|
+
Transports and serializers for messenger — amqp, taskiq, pydantic, joined with +:
|
|
80
|
+
|
|
81
|
+
shop — the webapp template
|
|
82
|
+
--------------------------
|
|
83
|
+
|
|
84
|
+
• directory: /home/ada/shop
|
|
85
|
+
• add-ons: orm (sqlite, postgres), messenger
|
|
86
|
+
• python: 3.13
|
|
87
|
+
The same, with no question asked:
|
|
88
|
+
uvx xtr-cli new shop --template=webapp --with=orm:sqlite+postgres,messenger --python=3.13 -n
|
|
89
|
+
|
|
90
|
+
Create it? [y/n] (y):
|
|
91
|
+
write pyproject.toml
|
|
92
|
+
…
|
|
93
|
+
run uv add -- 'xtr-dependency-injection[fastapi]>=4.0,<5' …
|
|
94
|
+
run uv add --dev -- 'xtr-recipes>=4.0,<5' pytest httpx ruff basedpyright ty
|
|
95
|
+
run uv run xtr-recipes recipes:sync
|
|
96
|
+
run git init --quiet
|
|
97
|
+
|
|
98
|
+
[OK] shop is ready in /home/ada/shop
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
cd shop
|
|
103
|
+
uv run shop-web # serve it
|
|
104
|
+
uv run shop debug:bundles # what is active
|
|
105
|
+
uv run pytest
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Commands
|
|
109
|
+
|
|
110
|
+
```sh
|
|
111
|
+
xtr project:new [<directory>] [options] # alias: new — into a new or empty directory
|
|
112
|
+
xtr project:init [options] # alias: init — into the current directory
|
|
113
|
+
xtr server:start [options] # alias: serve — run the project's web application
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
| Option | Default | |
|
|
117
|
+
|---|---|---|
|
|
118
|
+
| `--template` | asked; `cli` with `-n` | `cli`, `webapp`, or a directory holding a `template.toml` |
|
|
119
|
+
| `--name` | the directory's name | the distribution and console script; dashes become underscores for the import package |
|
|
120
|
+
| `--with` | asked; none with `-n` | add-ons, comma-separated: `orm:sqlite+postgres,messenger` |
|
|
121
|
+
| `--python` | the Python running this tool | the `3.<minor>` the project is pinned to; 3.11 or later |
|
|
122
|
+
| `--no-git` | | do not run `git init`; `init` never re-initialises an existing repository |
|
|
123
|
+
| `--dry-run` | | print every file and command, and write nothing |
|
|
124
|
+
| `--local-packages` | | install the xtr packages, editable, from a directory of their sources |
|
|
125
|
+
| `-n` | | ask nothing: every question takes its default |
|
|
126
|
+
|
|
127
|
+
**Questions fill in only what the command line left out**, and an option given is never asked.
|
|
128
|
+
With `-n` — or wherever nothing can answer, such as CI — nothing is asked at all. Before anything
|
|
129
|
+
is written the run prints the command that makes the same project with no question asked, so an
|
|
130
|
+
answered run can be repeated in a script.
|
|
131
|
+
|
|
132
|
+
The directory must be missing, or hold nothing but `.git` and editor metadata: nothing is ever
|
|
133
|
+
written over. A name that would not import — a keyword, a module of the standard library — is
|
|
134
|
+
refused before anything is written.
|
|
135
|
+
|
|
136
|
+
### What it runs
|
|
137
|
+
|
|
138
|
+
1. Writes the template's files.
|
|
139
|
+
2. `uv add` the template's packages and the add-ons; every unversioned xtr requirement is pinned
|
|
140
|
+
to this tool's major version, so the files and the packages are from the same release.
|
|
141
|
+
3. `uv add --dev` the tools: `xtr-recipes`, `pytest`, `ruff`, `basedpyright`, `ty`.
|
|
142
|
+
4. `uv run xtr-recipes recipes:sync` — each package's recipe writes `<app>/bundles.py`, its
|
|
143
|
+
configuration files, its `.env` and `.gitignore` blocks, and `xtr.lock`; the steps it cannot
|
|
144
|
+
make are printed.
|
|
145
|
+
5. `git init`; nothing is committed.
|
|
146
|
+
|
|
147
|
+
A step that fails stops the run; what was made so far is left in place, and the error names the
|
|
148
|
+
command to run by hand.
|
|
149
|
+
|
|
150
|
+
### Serving
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
cd shop
|
|
154
|
+
uvx xtr-cli serve # or `xtr serve` once installed
|
|
155
|
+
uvx xtr-cli serve --port 9000 --no-reload
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`serve` finds the project — the nearest `pyproject.toml` at or above the current directory, or
|
|
159
|
+
`--project-dir` — and its web entry point, then runs it in the project's own environment with
|
|
160
|
+
`uv run`. The server restarts on every source change; `--no-reload` turns that off.
|
|
161
|
+
|
|
162
|
+
| Option | Default | |
|
|
163
|
+
|---|---|---|
|
|
164
|
+
| `--project-dir` | the nearest project | the project to serve |
|
|
165
|
+
| `--entrypoint` | found in the project | a console script, or an ASGI application as `module:attribute` |
|
|
166
|
+
| `--host` | the entry point's own | where to listen |
|
|
167
|
+
| `--port` | the entry point's own | the port to listen on |
|
|
168
|
+
| `--reload` / `--no-reload` | `--reload` | restart on every source change |
|
|
169
|
+
| `--dry-run` | | print the command, and run nothing |
|
|
170
|
+
|
|
171
|
+
The web entry point is found by reading the project's files, never by importing them:
|
|
172
|
+
|
|
173
|
+
1. **A web console script** — a `[project.scripts]` entry whose module imports `uvicorn`,
|
|
174
|
+
`hypercorn` or `granian`, or whose name ends in `-web` or `-server`. It is run as it is, so
|
|
175
|
+
whatever it does before serving — loading the `.env` files, for one — still happens. The host,
|
|
176
|
+
the port and reloading reach it as `WEB_HOST`, `WEB_PORT` and `WEB_RELOAD=1`, which the
|
|
177
|
+
`webapp` template's `<name>-web` reads.
|
|
178
|
+
2. **An ASGI application** — only when there is no such script: a module-level
|
|
179
|
+
`FastAPI()`, `Starlette()` or `Litestar()`, handed to `uv run uvicorn module:attribute`, on
|
|
180
|
+
`127.0.0.1:8000` unless told otherwise. `uvicorn` must be installed in the project, and
|
|
181
|
+
nothing the application does at startup outside that module runs.
|
|
182
|
+
|
|
183
|
+
Several candidates are offered to choose from; with `-n` the command fails and lists them for
|
|
184
|
+
`--entrypoint`. Ctrl-C stops the server and waits for it to finish shutting down, so the
|
|
185
|
+
application's shutdown hooks run.
|
|
186
|
+
|
|
187
|
+
## Templates
|
|
188
|
+
|
|
189
|
+
| Template | Packages | Scripts |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `cli` | `xtr-dependency-injection`, `xtr-console[di]`, `xtr-dotenv[di,console]`, `xtr-logging[di]` | `<name>` |
|
|
192
|
+
| `webapp` | the same, `xtr-dependency-injection[fastapi]`, `xtr-http-kernel[logging,console]`, `fastapi`, `uvicorn` | `<name>`, `<name>-web` |
|
|
193
|
+
|
|
194
|
+
Both write `pyproject.toml` (with ruff, basedpyright, ty and pytest configured), `.python-version`,
|
|
195
|
+
`.gitignore`, `README.md`, `src/<app>/kernel.py`, `src/<app>/__main__.py`, an example
|
|
196
|
+
`app:hello` command and a test that builds the kernel. `webapp` adds `src/<app>/web/` — the
|
|
197
|
+
FastAPI application served with `setup(app, kernel)`, its routes and a uvicorn entry point — and
|
|
198
|
+
a test that requests the home page.
|
|
199
|
+
|
|
200
|
+
### A template of your own
|
|
201
|
+
|
|
202
|
+
`--template=<directory>` takes any directory holding a `template.toml`:
|
|
203
|
+
|
|
204
|
+
```toml
|
|
205
|
+
summary = "What it makes, in one line."
|
|
206
|
+
|
|
207
|
+
[dependencies]
|
|
208
|
+
packages = ["xtr-console[di]"] # uv add
|
|
209
|
+
dev = ["xtr-recipes", "pytest"] # uv add --dev; with xtr-recipes, the recipes are applied
|
|
210
|
+
|
|
211
|
+
[files]
|
|
212
|
+
# destination in the project = file under the template directory
|
|
213
|
+
"pyproject.toml" = "files/pyproject.toml.tmpl"
|
|
214
|
+
"src/${app}/__main__.py" = "files/main.py.tmpl"
|
|
215
|
+
|
|
216
|
+
[notes]
|
|
217
|
+
next = ["uv run ${name} list"] # printed once the project is made
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Destinations, files and notes substitute `${name}` (the distribution), `${app}` (the import
|
|
221
|
+
name), `${python}` (`3.13`) and `${minor}` (`13`); a literal `$` is written `$$`. An unknown
|
|
222
|
+
placeholder, table or key is an `InvalidTemplateError` before anything is written. Write only
|
|
223
|
+
what no recipe writes: the bundle list, `.env` and the ignore lines are the recipes'.
|
|
224
|
+
|
|
225
|
+
## Add-ons
|
|
226
|
+
|
|
227
|
+
Choosing an add-on only adds its requirement; the recipe the package ships configures it, the
|
|
228
|
+
way it would for a package added later with `xtr-recipes recipes:add`.
|
|
229
|
+
|
|
230
|
+
| Add-on | Installs | Variants |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `orm` | `xtr-orm[di,console]` | `sqlite` (default), `postgres`, `mysql` |
|
|
233
|
+
| `messenger` | `xtr-messenger[di,console]` | `amqp`, `taskiq`, `pydantic` |
|
|
234
|
+
| `scheduler` | `xtr-scheduler[di,console,cron]` | |
|
|
235
|
+
| `event-dispatcher` | `xtr-event-dispatcher[di,console]` | |
|
|
236
|
+
| `cache` | `xtr-cache[di,console]` | `redis` |
|
|
237
|
+
| `lock` | `xtr-lock[di]` | `redis` |
|
|
238
|
+
| `rate-limiter` | `xtr-rate-limiter[di,cache,lock]` | `redis` |
|
|
239
|
+
| `storage` | `xtr-storage[di]` | `s3`, `gcs` |
|
|
240
|
+
| `security` | `xtr-security[console]` — `webapp` only | |
|
|
241
|
+
| `jwt` | `xtr-security-jwt[console]` — `webapp` only; brings `security` | |
|
|
242
|
+
|
|
243
|
+
`jwt` boots without a key and signs nothing until one is there: run
|
|
244
|
+
`uv run <name> jwt:generate-keypair --kid jwt --output-dir secrets` for development, and mount
|
|
245
|
+
the deployment's own key in production. No key is ever minted for you.
|
|
246
|
+
|
|
247
|
+
Variants follow the name after `:`, joined with `+`: `orm:sqlite+postgres`. A name alone takes
|
|
248
|
+
the defaults; `messenger:` picks none. Asked interactively, an add-on with variants is followed by
|
|
249
|
+
a question for them.
|
|
250
|
+
|
|
251
|
+
## Errors
|
|
252
|
+
|
|
253
|
+
Everything this library raises derives from `CliError`, and carries what went wrong as typed
|
|
254
|
+
attributes. A command reports it and exits `1`.
|
|
255
|
+
|
|
256
|
+
| Error | Raised when |
|
|
257
|
+
|---|---|
|
|
258
|
+
| `DirectoryNotEmptyError` | the directory is a file, or holds anything but version-control and editor metadata |
|
|
259
|
+
| `AmbiguousWebEntryPointError` | `serve` found several web entry points, and no question may be asked |
|
|
260
|
+
| `InvalidProjectNameError` | the name normalises to nothing, or to a keyword, a non-identifier or a standard-library module |
|
|
261
|
+
| `InvalidTemplateError` | a `template.toml` does not parse, holds an unknown key, names a missing file, or a placeholder is unknown |
|
|
262
|
+
| `ProjectNotFoundError` | `serve` was run outside a project |
|
|
263
|
+
| `StepFailedError` | `uv` or `git` exited with a failure; the later steps were not run |
|
|
264
|
+
| `UnknownAddOnError` | `--with` names an unknown add-on, one the template cannot take, one twice, or an unknown variant |
|
|
265
|
+
| `UnknownTemplateError` | `--template` is neither a shipped template nor a directory holding one |
|
|
266
|
+
| `UnsupportedPythonError` | `--python` is not `3.<minor>` with a minor of 11 or more |
|
|
267
|
+
| `WebEntryPointNotFoundError` | the project has nothing to serve, or `--entrypoint` names nothing it has |
|
|
268
|
+
|
|
269
|
+
## Layout
|
|
270
|
+
|
|
271
|
+
```
|
|
272
|
+
xtr_cli/
|
|
273
|
+
├── command/ project:new, project:init and server:start
|
|
274
|
+
├── command_support.py what both commands share: questions, plan, apply, report
|
|
275
|
+
├── project_questions.py what the command line left out, asked
|
|
276
|
+
├── project_name.py the distribution and import names
|
|
277
|
+
├── project_settings.py everything decided, and the command line repeating it
|
|
278
|
+
├── template.py a template.toml and its files
|
|
279
|
+
├── template_catalog.py the shipped templates, or a directory
|
|
280
|
+
├── add_on.py one add-on: a name standing for a requirement
|
|
281
|
+
├── add_on_catalog.py the add-ons, and how --with names them
|
|
282
|
+
├── scaffolder.py settings into a plan, a plan into a project
|
|
283
|
+
├── scaffold_plan.py the files and commands, before anything is made
|
|
284
|
+
├── web_entry_point.py what serves an application, and the command starting it
|
|
285
|
+
├── web_entry_point_finder.py the project around a directory, and its web entry point
|
|
286
|
+
├── subprocess_runner.py uv, git and the server, run as real processes
|
|
287
|
+
├── templates/ cli/ and webapp/
|
|
288
|
+
└── exception/ CliError, the root of everything this library raises
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## Development
|
|
292
|
+
|
|
293
|
+
Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
|
|
294
|
+
`packages/xtr-cli`; run the commands below from there.
|
|
295
|
+
|
|
296
|
+
```sh
|
|
297
|
+
uv sync --all-extras
|
|
298
|
+
uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
`tests/integration/test_scaffold.py` makes each template for real, from the working tree's
|
|
302
|
+
packages, and runs the project's own checks and commands. It needs `uv` and the network for the
|
|
303
|
+
third-party packages, and is skipped unless `XTR_CLI_SCAFFOLD=1` is set.
|
|
304
|
+
|
|
305
|
+
## License
|
|
306
|
+
|
|
307
|
+
MIT — see [LICENSE](LICENSE).
|
xtr_cli-4.0.2/README.md
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# xtr-cli
|
|
4
|
+
|
|
5
|
+
**A new xtr application — a console application or a web application — ready to run in one command, and served in another.**
|
|
6
|
+
|
|
7
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
8
|
+
<img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
|
|
9
|
+
<img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
10
|
+
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Why?
|
|
16
|
+
|
|
17
|
+
An application on the xtr packages starts the same way every time: a project manifest, a kernel,
|
|
18
|
+
an entry point that loads the environment and runs the console, a web entry point that serves
|
|
19
|
+
the application from the same kernel. Then each package's *Use in an application* steps — the
|
|
20
|
+
bundle list, the configuration files, the `.env` and ignore lines.
|
|
21
|
+
|
|
22
|
+
The packages already describe their own steps as recipes, which
|
|
23
|
+
[xtr-recipes](../xtr-recipes) applies. So this tool writes only what no recipe can — the
|
|
24
|
+
manifest, the kernel and the entry points — installs the packages with `uv`, and lets each
|
|
25
|
+
package's recipe configure itself. The project it leaves boots, passes its own tests, and is
|
|
26
|
+
already in the state `recipes:sync --check` expects.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
Nothing to install: run it with `uvx`.
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
uvx xtr-cli new shop
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Or install it once, and call it `xtr`:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
uv tool install xtr-cli
|
|
40
|
+
xtr new shop
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/) on the `PATH`, and `git` unless
|
|
44
|
+
`--no-git` is given.
|
|
45
|
+
|
|
46
|
+
## Quick start
|
|
47
|
+
|
|
48
|
+
```console
|
|
49
|
+
$ uvx xtr-cli new shop
|
|
50
|
+
• cli — A console application: a kernel, its commands, the layered .env files and logging.
|
|
51
|
+
• webapp — A web application: FastAPI served from the kernel, with the console beside it.
|
|
52
|
+
Template [cli/webapp] (cli): webapp
|
|
53
|
+
• orm — Database engines, sessions per unit of work and versioned migrations.
|
|
54
|
+
• messenger — A message bus: handlers, middleware, transports and consumers.
|
|
55
|
+
…
|
|
56
|
+
Add-ons, comma-separated (empty for none): orm, messenger
|
|
57
|
+
Database drivers for orm — sqlite, postgres, mysql, joined with + (sqlite): sqlite+postgres
|
|
58
|
+
Transports and serializers for messenger — amqp, taskiq, pydantic, joined with +:
|
|
59
|
+
|
|
60
|
+
shop — the webapp template
|
|
61
|
+
--------------------------
|
|
62
|
+
|
|
63
|
+
• directory: /home/ada/shop
|
|
64
|
+
• add-ons: orm (sqlite, postgres), messenger
|
|
65
|
+
• python: 3.13
|
|
66
|
+
The same, with no question asked:
|
|
67
|
+
uvx xtr-cli new shop --template=webapp --with=orm:sqlite+postgres,messenger --python=3.13 -n
|
|
68
|
+
|
|
69
|
+
Create it? [y/n] (y):
|
|
70
|
+
write pyproject.toml
|
|
71
|
+
…
|
|
72
|
+
run uv add -- 'xtr-dependency-injection[fastapi]>=4.0,<5' …
|
|
73
|
+
run uv add --dev -- 'xtr-recipes>=4.0,<5' pytest httpx ruff basedpyright ty
|
|
74
|
+
run uv run xtr-recipes recipes:sync
|
|
75
|
+
run git init --quiet
|
|
76
|
+
|
|
77
|
+
[OK] shop is ready in /home/ada/shop
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```sh
|
|
81
|
+
cd shop
|
|
82
|
+
uv run shop-web # serve it
|
|
83
|
+
uv run shop debug:bundles # what is active
|
|
84
|
+
uv run pytest
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Commands
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
xtr project:new [<directory>] [options] # alias: new — into a new or empty directory
|
|
91
|
+
xtr project:init [options] # alias: init — into the current directory
|
|
92
|
+
xtr server:start [options] # alias: serve — run the project's web application
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Option | Default | |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| `--template` | asked; `cli` with `-n` | `cli`, `webapp`, or a directory holding a `template.toml` |
|
|
98
|
+
| `--name` | the directory's name | the distribution and console script; dashes become underscores for the import package |
|
|
99
|
+
| `--with` | asked; none with `-n` | add-ons, comma-separated: `orm:sqlite+postgres,messenger` |
|
|
100
|
+
| `--python` | the Python running this tool | the `3.<minor>` the project is pinned to; 3.11 or later |
|
|
101
|
+
| `--no-git` | | do not run `git init`; `init` never re-initialises an existing repository |
|
|
102
|
+
| `--dry-run` | | print every file and command, and write nothing |
|
|
103
|
+
| `--local-packages` | | install the xtr packages, editable, from a directory of their sources |
|
|
104
|
+
| `-n` | | ask nothing: every question takes its default |
|
|
105
|
+
|
|
106
|
+
**Questions fill in only what the command line left out**, and an option given is never asked.
|
|
107
|
+
With `-n` — or wherever nothing can answer, such as CI — nothing is asked at all. Before anything
|
|
108
|
+
is written the run prints the command that makes the same project with no question asked, so an
|
|
109
|
+
answered run can be repeated in a script.
|
|
110
|
+
|
|
111
|
+
The directory must be missing, or hold nothing but `.git` and editor metadata: nothing is ever
|
|
112
|
+
written over. A name that would not import — a keyword, a module of the standard library — is
|
|
113
|
+
refused before anything is written.
|
|
114
|
+
|
|
115
|
+
### What it runs
|
|
116
|
+
|
|
117
|
+
1. Writes the template's files.
|
|
118
|
+
2. `uv add` the template's packages and the add-ons; every unversioned xtr requirement is pinned
|
|
119
|
+
to this tool's major version, so the files and the packages are from the same release.
|
|
120
|
+
3. `uv add --dev` the tools: `xtr-recipes`, `pytest`, `ruff`, `basedpyright`, `ty`.
|
|
121
|
+
4. `uv run xtr-recipes recipes:sync` — each package's recipe writes `<app>/bundles.py`, its
|
|
122
|
+
configuration files, its `.env` and `.gitignore` blocks, and `xtr.lock`; the steps it cannot
|
|
123
|
+
make are printed.
|
|
124
|
+
5. `git init`; nothing is committed.
|
|
125
|
+
|
|
126
|
+
A step that fails stops the run; what was made so far is left in place, and the error names the
|
|
127
|
+
command to run by hand.
|
|
128
|
+
|
|
129
|
+
### Serving
|
|
130
|
+
|
|
131
|
+
```sh
|
|
132
|
+
cd shop
|
|
133
|
+
uvx xtr-cli serve # or `xtr serve` once installed
|
|
134
|
+
uvx xtr-cli serve --port 9000 --no-reload
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`serve` finds the project — the nearest `pyproject.toml` at or above the current directory, or
|
|
138
|
+
`--project-dir` — and its web entry point, then runs it in the project's own environment with
|
|
139
|
+
`uv run`. The server restarts on every source change; `--no-reload` turns that off.
|
|
140
|
+
|
|
141
|
+
| Option | Default | |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| `--project-dir` | the nearest project | the project to serve |
|
|
144
|
+
| `--entrypoint` | found in the project | a console script, or an ASGI application as `module:attribute` |
|
|
145
|
+
| `--host` | the entry point's own | where to listen |
|
|
146
|
+
| `--port` | the entry point's own | the port to listen on |
|
|
147
|
+
| `--reload` / `--no-reload` | `--reload` | restart on every source change |
|
|
148
|
+
| `--dry-run` | | print the command, and run nothing |
|
|
149
|
+
|
|
150
|
+
The web entry point is found by reading the project's files, never by importing them:
|
|
151
|
+
|
|
152
|
+
1. **A web console script** — a `[project.scripts]` entry whose module imports `uvicorn`,
|
|
153
|
+
`hypercorn` or `granian`, or whose name ends in `-web` or `-server`. It is run as it is, so
|
|
154
|
+
whatever it does before serving — loading the `.env` files, for one — still happens. The host,
|
|
155
|
+
the port and reloading reach it as `WEB_HOST`, `WEB_PORT` and `WEB_RELOAD=1`, which the
|
|
156
|
+
`webapp` template's `<name>-web` reads.
|
|
157
|
+
2. **An ASGI application** — only when there is no such script: a module-level
|
|
158
|
+
`FastAPI()`, `Starlette()` or `Litestar()`, handed to `uv run uvicorn module:attribute`, on
|
|
159
|
+
`127.0.0.1:8000` unless told otherwise. `uvicorn` must be installed in the project, and
|
|
160
|
+
nothing the application does at startup outside that module runs.
|
|
161
|
+
|
|
162
|
+
Several candidates are offered to choose from; with `-n` the command fails and lists them for
|
|
163
|
+
`--entrypoint`. Ctrl-C stops the server and waits for it to finish shutting down, so the
|
|
164
|
+
application's shutdown hooks run.
|
|
165
|
+
|
|
166
|
+
## Templates
|
|
167
|
+
|
|
168
|
+
| Template | Packages | Scripts |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `cli` | `xtr-dependency-injection`, `xtr-console[di]`, `xtr-dotenv[di,console]`, `xtr-logging[di]` | `<name>` |
|
|
171
|
+
| `webapp` | the same, `xtr-dependency-injection[fastapi]`, `xtr-http-kernel[logging,console]`, `fastapi`, `uvicorn` | `<name>`, `<name>-web` |
|
|
172
|
+
|
|
173
|
+
Both write `pyproject.toml` (with ruff, basedpyright, ty and pytest configured), `.python-version`,
|
|
174
|
+
`.gitignore`, `README.md`, `src/<app>/kernel.py`, `src/<app>/__main__.py`, an example
|
|
175
|
+
`app:hello` command and a test that builds the kernel. `webapp` adds `src/<app>/web/` — the
|
|
176
|
+
FastAPI application served with `setup(app, kernel)`, its routes and a uvicorn entry point — and
|
|
177
|
+
a test that requests the home page.
|
|
178
|
+
|
|
179
|
+
### A template of your own
|
|
180
|
+
|
|
181
|
+
`--template=<directory>` takes any directory holding a `template.toml`:
|
|
182
|
+
|
|
183
|
+
```toml
|
|
184
|
+
summary = "What it makes, in one line."
|
|
185
|
+
|
|
186
|
+
[dependencies]
|
|
187
|
+
packages = ["xtr-console[di]"] # uv add
|
|
188
|
+
dev = ["xtr-recipes", "pytest"] # uv add --dev; with xtr-recipes, the recipes are applied
|
|
189
|
+
|
|
190
|
+
[files]
|
|
191
|
+
# destination in the project = file under the template directory
|
|
192
|
+
"pyproject.toml" = "files/pyproject.toml.tmpl"
|
|
193
|
+
"src/${app}/__main__.py" = "files/main.py.tmpl"
|
|
194
|
+
|
|
195
|
+
[notes]
|
|
196
|
+
next = ["uv run ${name} list"] # printed once the project is made
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Destinations, files and notes substitute `${name}` (the distribution), `${app}` (the import
|
|
200
|
+
name), `${python}` (`3.13`) and `${minor}` (`13`); a literal `$` is written `$$`. An unknown
|
|
201
|
+
placeholder, table or key is an `InvalidTemplateError` before anything is written. Write only
|
|
202
|
+
what no recipe writes: the bundle list, `.env` and the ignore lines are the recipes'.
|
|
203
|
+
|
|
204
|
+
## Add-ons
|
|
205
|
+
|
|
206
|
+
Choosing an add-on only adds its requirement; the recipe the package ships configures it, the
|
|
207
|
+
way it would for a package added later with `xtr-recipes recipes:add`.
|
|
208
|
+
|
|
209
|
+
| Add-on | Installs | Variants |
|
|
210
|
+
|---|---|---|
|
|
211
|
+
| `orm` | `xtr-orm[di,console]` | `sqlite` (default), `postgres`, `mysql` |
|
|
212
|
+
| `messenger` | `xtr-messenger[di,console]` | `amqp`, `taskiq`, `pydantic` |
|
|
213
|
+
| `scheduler` | `xtr-scheduler[di,console,cron]` | |
|
|
214
|
+
| `event-dispatcher` | `xtr-event-dispatcher[di,console]` | |
|
|
215
|
+
| `cache` | `xtr-cache[di,console]` | `redis` |
|
|
216
|
+
| `lock` | `xtr-lock[di]` | `redis` |
|
|
217
|
+
| `rate-limiter` | `xtr-rate-limiter[di,cache,lock]` | `redis` |
|
|
218
|
+
| `storage` | `xtr-storage[di]` | `s3`, `gcs` |
|
|
219
|
+
| `security` | `xtr-security[console]` — `webapp` only | |
|
|
220
|
+
| `jwt` | `xtr-security-jwt[console]` — `webapp` only; brings `security` | |
|
|
221
|
+
|
|
222
|
+
`jwt` boots without a key and signs nothing until one is there: run
|
|
223
|
+
`uv run <name> jwt:generate-keypair --kid jwt --output-dir secrets` for development, and mount
|
|
224
|
+
the deployment's own key in production. No key is ever minted for you.
|
|
225
|
+
|
|
226
|
+
Variants follow the name after `:`, joined with `+`: `orm:sqlite+postgres`. A name alone takes
|
|
227
|
+
the defaults; `messenger:` picks none. Asked interactively, an add-on with variants is followed by
|
|
228
|
+
a question for them.
|
|
229
|
+
|
|
230
|
+
## Errors
|
|
231
|
+
|
|
232
|
+
Everything this library raises derives from `CliError`, and carries what went wrong as typed
|
|
233
|
+
attributes. A command reports it and exits `1`.
|
|
234
|
+
|
|
235
|
+
| Error | Raised when |
|
|
236
|
+
|---|---|
|
|
237
|
+
| `DirectoryNotEmptyError` | the directory is a file, or holds anything but version-control and editor metadata |
|
|
238
|
+
| `AmbiguousWebEntryPointError` | `serve` found several web entry points, and no question may be asked |
|
|
239
|
+
| `InvalidProjectNameError` | the name normalises to nothing, or to a keyword, a non-identifier or a standard-library module |
|
|
240
|
+
| `InvalidTemplateError` | a `template.toml` does not parse, holds an unknown key, names a missing file, or a placeholder is unknown |
|
|
241
|
+
| `ProjectNotFoundError` | `serve` was run outside a project |
|
|
242
|
+
| `StepFailedError` | `uv` or `git` exited with a failure; the later steps were not run |
|
|
243
|
+
| `UnknownAddOnError` | `--with` names an unknown add-on, one the template cannot take, one twice, or an unknown variant |
|
|
244
|
+
| `UnknownTemplateError` | `--template` is neither a shipped template nor a directory holding one |
|
|
245
|
+
| `UnsupportedPythonError` | `--python` is not `3.<minor>` with a minor of 11 or more |
|
|
246
|
+
| `WebEntryPointNotFoundError` | the project has nothing to serve, or `--entrypoint` names nothing it has |
|
|
247
|
+
|
|
248
|
+
## Layout
|
|
249
|
+
|
|
250
|
+
```
|
|
251
|
+
xtr_cli/
|
|
252
|
+
├── command/ project:new, project:init and server:start
|
|
253
|
+
├── command_support.py what both commands share: questions, plan, apply, report
|
|
254
|
+
├── project_questions.py what the command line left out, asked
|
|
255
|
+
├── project_name.py the distribution and import names
|
|
256
|
+
├── project_settings.py everything decided, and the command line repeating it
|
|
257
|
+
├── template.py a template.toml and its files
|
|
258
|
+
├── template_catalog.py the shipped templates, or a directory
|
|
259
|
+
├── add_on.py one add-on: a name standing for a requirement
|
|
260
|
+
├── add_on_catalog.py the add-ons, and how --with names them
|
|
261
|
+
├── scaffolder.py settings into a plan, a plan into a project
|
|
262
|
+
├── scaffold_plan.py the files and commands, before anything is made
|
|
263
|
+
├── web_entry_point.py what serves an application, and the command starting it
|
|
264
|
+
├── web_entry_point_finder.py the project around a directory, and its web entry point
|
|
265
|
+
├── subprocess_runner.py uv, git and the server, run as real processes
|
|
266
|
+
├── templates/ cli/ and webapp/
|
|
267
|
+
└── exception/ CliError, the root of everything this library raises
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Development
|
|
271
|
+
|
|
272
|
+
Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
|
|
273
|
+
`packages/xtr-cli`; run the commands below from there.
|
|
274
|
+
|
|
275
|
+
```sh
|
|
276
|
+
uv sync --all-extras
|
|
277
|
+
uv run ruff check && uv run ruff format --check && uv run basedpyright && uv run ty check && uv run pytest
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`tests/integration/test_scaffold.py` makes each template for real, from the working tree's
|
|
281
|
+
packages, and runs the project's own checks and commands. It needs `uv` and the network for the
|
|
282
|
+
third-party packages, and is skipped unless `XTR_CLI_SCAFFOLD=1` is set.
|
|
283
|
+
|
|
284
|
+
## License
|
|
285
|
+
|
|
286
|
+
MIT — see [LICENSE](LICENSE).
|