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.
Files changed (73) hide show
  1. xtr_cli-4.0.2/LICENSE +21 -0
  2. xtr_cli-4.0.2/PKG-INFO +307 -0
  3. xtr_cli-4.0.2/README.md +286 -0
  4. xtr_cli-4.0.2/pyproject.toml +161 -0
  5. xtr_cli-4.0.2/pyproject.toml.orig +162 -0
  6. xtr_cli-4.0.2/src/xtr_cli/__init__.py +79 -0
  7. xtr_cli-4.0.2/src/xtr_cli/__main__.py +28 -0
  8. xtr_cli-4.0.2/src/xtr_cli/add_on.py +52 -0
  9. xtr_cli-4.0.2/src/xtr_cli/add_on_catalog.py +167 -0
  10. xtr_cli-4.0.2/src/xtr_cli/add_on_choice.py +40 -0
  11. xtr_cli-4.0.2/src/xtr_cli/command/__init__.py +14 -0
  12. xtr_cli-4.0.2/src/xtr_cli/command/init_command.py +69 -0
  13. xtr_cli-4.0.2/src/xtr_cli/command/new_command.py +72 -0
  14. xtr_cli-4.0.2/src/xtr_cli/command/serve_command.py +114 -0
  15. xtr_cli-4.0.2/src/xtr_cli/command_support.py +189 -0
  16. xtr_cli-4.0.2/src/xtr_cli/exception/__init__.py +34 -0
  17. xtr_cli-4.0.2/src/xtr_cli/exception/ambiguous_web_entry_point_error.py +27 -0
  18. xtr_cli-4.0.2/src/xtr_cli/exception/cli_error.py +14 -0
  19. xtr_cli-4.0.2/src/xtr_cli/exception/directory_not_empty_error.py +35 -0
  20. xtr_cli-4.0.2/src/xtr_cli/exception/invalid_project_name_error.py +33 -0
  21. xtr_cli-4.0.2/src/xtr_cli/exception/invalid_template_error.py +28 -0
  22. xtr_cli-4.0.2/src/xtr_cli/exception/project_not_found_error.py +28 -0
  23. xtr_cli-4.0.2/src/xtr_cli/exception/step_failed_error.py +38 -0
  24. xtr_cli-4.0.2/src/xtr_cli/exception/unknown_add_on_error.py +28 -0
  25. xtr_cli-4.0.2/src/xtr_cli/exception/unknown_template_error.py +29 -0
  26. xtr_cli-4.0.2/src/xtr_cli/exception/unsupported_python_error.py +28 -0
  27. xtr_cli-4.0.2/src/xtr_cli/exception/web_entry_point_not_found_error.py +31 -0
  28. xtr_cli-4.0.2/src/xtr_cli/process_runner_interface.py +29 -0
  29. xtr_cli-4.0.2/src/xtr_cli/project_name.py +55 -0
  30. xtr_cli-4.0.2/src/xtr_cli/project_questions.py +108 -0
  31. xtr_cli-4.0.2/src/xtr_cli/project_settings.py +89 -0
  32. xtr_cli-4.0.2/src/xtr_cli/py.typed +0 -0
  33. xtr_cli-4.0.2/src/xtr_cli/scaffold_plan.py +41 -0
  34. xtr_cli-4.0.2/src/xtr_cli/scaffolder.py +159 -0
  35. xtr_cli-4.0.2/src/xtr_cli/subprocess_runner.py +47 -0
  36. xtr_cli-4.0.2/src/xtr_cli/template.py +178 -0
  37. xtr_cli-4.0.2/src/xtr_cli/template_catalog.py +62 -0
  38. xtr_cli-4.0.2/src/xtr_cli/templates/__init__.py +10 -0
  39. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/README.md.tmpl +44 -0
  40. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/__init__.py.tmpl +5 -0
  41. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/__main__.py.tmpl +23 -0
  42. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/command/__init__.py.tmpl +5 -0
  43. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/command/hello_command.py.tmpl +19 -0
  44. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/kernel.py.tmpl +34 -0
  45. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/app/py.typed.tmpl +0 -0
  46. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/gitignore.tmpl +14 -0
  47. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/pyproject.toml.tmpl +59 -0
  48. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/python-version.tmpl +1 -0
  49. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/tests/__init__.py.tmpl +0 -0
  50. xtr_cli-4.0.2/src/xtr_cli/templates/cli/files/tests/test_kernel.py.tmpl +9 -0
  51. xtr_cli-4.0.2/src/xtr_cli/templates/cli/template.toml +34 -0
  52. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/README.md.tmpl +49 -0
  53. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/__init__.py.tmpl +5 -0
  54. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/__main__.py.tmpl +23 -0
  55. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/command/__init__.py.tmpl +5 -0
  56. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/command/hello_command.py.tmpl +19 -0
  57. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/config/__init__.py.tmpl +5 -0
  58. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/config/http_kernel.py.tmpl +19 -0
  59. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/kernel.py.tmpl +37 -0
  60. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/py.typed.tmpl +0 -0
  61. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/web/__init__.py.tmpl +5 -0
  62. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/web/__main__.py.tmpl +42 -0
  63. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/web/app.py.tmpl +23 -0
  64. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/app/web/routes.py.tmpl +15 -0
  65. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/gitignore.tmpl +14 -0
  66. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/pyproject.toml.tmpl +60 -0
  67. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/python-version.tmpl +1 -0
  68. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/tests/__init__.py.tmpl +0 -0
  69. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/tests/test_kernel.py.tmpl +9 -0
  70. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/files/tests/test_web.py.tmpl +28 -0
  71. xtr_cli-4.0.2/src/xtr_cli/templates/webapp/template.toml +45 -0
  72. xtr_cli-4.0.2/src/xtr_cli/web_entry_point.py +76 -0
  73. 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).
@@ -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).