xtr-cli 4.0.2__py3-none-any.whl
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/__init__.py +79 -0
- xtr_cli/__main__.py +28 -0
- xtr_cli/add_on.py +52 -0
- xtr_cli/add_on_catalog.py +167 -0
- xtr_cli/add_on_choice.py +40 -0
- xtr_cli/command/__init__.py +14 -0
- xtr_cli/command/init_command.py +69 -0
- xtr_cli/command/new_command.py +72 -0
- xtr_cli/command/serve_command.py +114 -0
- xtr_cli/command_support.py +189 -0
- xtr_cli/exception/__init__.py +34 -0
- xtr_cli/exception/ambiguous_web_entry_point_error.py +27 -0
- xtr_cli/exception/cli_error.py +14 -0
- xtr_cli/exception/directory_not_empty_error.py +35 -0
- xtr_cli/exception/invalid_project_name_error.py +33 -0
- xtr_cli/exception/invalid_template_error.py +28 -0
- xtr_cli/exception/project_not_found_error.py +28 -0
- xtr_cli/exception/step_failed_error.py +38 -0
- xtr_cli/exception/unknown_add_on_error.py +28 -0
- xtr_cli/exception/unknown_template_error.py +29 -0
- xtr_cli/exception/unsupported_python_error.py +28 -0
- xtr_cli/exception/web_entry_point_not_found_error.py +31 -0
- xtr_cli/process_runner_interface.py +29 -0
- xtr_cli/project_name.py +55 -0
- xtr_cli/project_questions.py +108 -0
- xtr_cli/project_settings.py +89 -0
- xtr_cli/py.typed +0 -0
- xtr_cli/scaffold_plan.py +41 -0
- xtr_cli/scaffolder.py +159 -0
- xtr_cli/subprocess_runner.py +47 -0
- xtr_cli/template.py +178 -0
- xtr_cli/template_catalog.py +62 -0
- xtr_cli/templates/__init__.py +10 -0
- xtr_cli/templates/cli/files/README.md.tmpl +44 -0
- xtr_cli/templates/cli/files/app/__init__.py.tmpl +5 -0
- xtr_cli/templates/cli/files/app/__main__.py.tmpl +23 -0
- xtr_cli/templates/cli/files/app/command/__init__.py.tmpl +5 -0
- xtr_cli/templates/cli/files/app/command/hello_command.py.tmpl +19 -0
- xtr_cli/templates/cli/files/app/kernel.py.tmpl +34 -0
- xtr_cli/templates/cli/files/app/py.typed.tmpl +0 -0
- xtr_cli/templates/cli/files/gitignore.tmpl +14 -0
- xtr_cli/templates/cli/files/pyproject.toml.tmpl +59 -0
- xtr_cli/templates/cli/files/python-version.tmpl +1 -0
- xtr_cli/templates/cli/files/tests/__init__.py.tmpl +0 -0
- xtr_cli/templates/cli/files/tests/test_kernel.py.tmpl +9 -0
- xtr_cli/templates/cli/template.toml +34 -0
- xtr_cli/templates/webapp/files/README.md.tmpl +49 -0
- xtr_cli/templates/webapp/files/app/__init__.py.tmpl +5 -0
- xtr_cli/templates/webapp/files/app/__main__.py.tmpl +23 -0
- xtr_cli/templates/webapp/files/app/command/__init__.py.tmpl +5 -0
- xtr_cli/templates/webapp/files/app/command/hello_command.py.tmpl +19 -0
- xtr_cli/templates/webapp/files/app/config/__init__.py.tmpl +5 -0
- xtr_cli/templates/webapp/files/app/config/http_kernel.py.tmpl +19 -0
- xtr_cli/templates/webapp/files/app/kernel.py.tmpl +37 -0
- xtr_cli/templates/webapp/files/app/py.typed.tmpl +0 -0
- xtr_cli/templates/webapp/files/app/web/__init__.py.tmpl +5 -0
- xtr_cli/templates/webapp/files/app/web/__main__.py.tmpl +42 -0
- xtr_cli/templates/webapp/files/app/web/app.py.tmpl +23 -0
- xtr_cli/templates/webapp/files/app/web/routes.py.tmpl +15 -0
- xtr_cli/templates/webapp/files/gitignore.tmpl +14 -0
- xtr_cli/templates/webapp/files/pyproject.toml.tmpl +60 -0
- xtr_cli/templates/webapp/files/python-version.tmpl +1 -0
- xtr_cli/templates/webapp/files/tests/__init__.py.tmpl +0 -0
- xtr_cli/templates/webapp/files/tests/test_kernel.py.tmpl +9 -0
- xtr_cli/templates/webapp/files/tests/test_web.py.tmpl +28 -0
- xtr_cli/templates/webapp/template.toml +45 -0
- xtr_cli/web_entry_point.py +76 -0
- xtr_cli/web_entry_point_finder.py +194 -0
- xtr_cli-4.0.2.dist-info/METADATA +307 -0
- xtr_cli-4.0.2.dist-info/RECORD +73 -0
- xtr_cli-4.0.2.dist-info/WHEEL +4 -0
- xtr_cli-4.0.2.dist-info/entry_points.txt +4 -0
- xtr_cli-4.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""TemplateCatalog: the templates shipped with this tool, and any directory holding one."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.resources import files
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING, final
|
|
8
|
+
|
|
9
|
+
from .exception import UnknownTemplateError
|
|
10
|
+
from .template import MANIFEST, Template
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from importlib.resources.abc import Traversable
|
|
14
|
+
|
|
15
|
+
__all__ = ["TemplateCatalog"]
|
|
16
|
+
|
|
17
|
+
_SHIPPED = "xtr_cli.templates"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@final
|
|
21
|
+
class TemplateCatalog:
|
|
22
|
+
"""Finds a template by the name it ships under, or by a directory on disk.
|
|
23
|
+
|
|
24
|
+
The shipped templates live inside this package, so they are the ones
|
|
25
|
+
written for the packages of the same release.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
__slots__ = ("_root",)
|
|
29
|
+
|
|
30
|
+
def __init__(self, root: Traversable) -> None:
|
|
31
|
+
"""Offer the templates in the subdirectories of ``root``."""
|
|
32
|
+
self._root = root
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def default(cls) -> TemplateCatalog:
|
|
36
|
+
"""Return the catalog of the templates this tool ships."""
|
|
37
|
+
return cls(files(_SHIPPED))
|
|
38
|
+
|
|
39
|
+
def names(self) -> tuple[str, ...]:
|
|
40
|
+
"""Return the names of the shipped templates, sorted."""
|
|
41
|
+
return tuple(
|
|
42
|
+
sorted(
|
|
43
|
+
entry.name
|
|
44
|
+
for entry in self._root.iterdir()
|
|
45
|
+
if entry.is_dir() and entry.joinpath(MANIFEST).is_file()
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def load(self, template: str) -> Template:
|
|
50
|
+
"""Return the shipped template called ``template``, or the one in that directory.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
UnknownTemplateError: When ``template`` is neither a shipped name
|
|
54
|
+
nor a directory holding a ``template.toml``.
|
|
55
|
+
InvalidTemplateError: When the template found cannot be read.
|
|
56
|
+
"""
|
|
57
|
+
if template in self.names():
|
|
58
|
+
return Template.load(self._root / template, template)
|
|
59
|
+
directory = Path(template).expanduser()
|
|
60
|
+
if directory.joinpath(MANIFEST).is_file():
|
|
61
|
+
return Template.load(directory, directory.resolve().name)
|
|
62
|
+
raise UnknownTemplateError(template, self.names())
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""The templates this tool ships, one directory each, read as package data.
|
|
2
|
+
|
|
3
|
+
A real package so the build carries every ``template.toml`` and ``*.tmpl``
|
|
4
|
+
file beside it in the wheel. Nothing here is imported or run: the files are
|
|
5
|
+
templates, named ``*.tmpl`` so they are never linted or type-checked as code.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# ${name}
|
|
2
|
+
|
|
3
|
+
A console application on the xtr packages.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uv run ${name} # every command
|
|
9
|
+
uv run ${name} app:hello Ada # the example command
|
|
10
|
+
uv run ${name} debug:bundles # the bundles and whether each is active
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The environment is read from `.env`, then `.env.local`, then `.env.<APP_ENV>` and
|
|
14
|
+
`.env.<APP_ENV>.local`; a variable set in the shell wins. Keep machine-specific values in
|
|
15
|
+
`.env.local`, which is not committed.
|
|
16
|
+
|
|
17
|
+
## Layout
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/${app}/
|
|
21
|
+
├── __main__.py the console entry point
|
|
22
|
+
├── kernel.py the kernel every entry point builds, and the environment loader
|
|
23
|
+
├── bundles.py the active bundles — written by xtr-recipes
|
|
24
|
+
└── command/ the console commands
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Add a package
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
uv run xtr-recipes recipes:add "xtr-messenger[di,console]"
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
It installs the package and applies its recipe — the bundle, its configuration file, its
|
|
34
|
+
environment variables — and prints any step left to do by hand. `xtr.lock` records what was
|
|
35
|
+
applied; commit it.
|
|
36
|
+
|
|
37
|
+
## Check
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
uv run pytest
|
|
41
|
+
uv run ruff check && uv run ruff format --check
|
|
42
|
+
uv run basedpyright && uv run ty check
|
|
43
|
+
uv run xtr-recipes recipes:sync --check
|
|
44
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The console entry point: ``uv run ${name} <command>`` or ``python -m ${app} <command>``.
|
|
2
|
+
|
|
3
|
+
``*.__main__`` modules are never scanned by the kernel, so importing the application never
|
|
4
|
+
runs it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from xtr_console.bundle import console
|
|
10
|
+
|
|
11
|
+
from ${app}.kernel import kernel, load_environment
|
|
12
|
+
|
|
13
|
+
__all__ = ["main"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
"""Load the environment, then boot, run the console, shut down and exit with its code."""
|
|
18
|
+
load_environment()
|
|
19
|
+
raise SystemExit(kernel.run(console))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""``app:hello``: the example command — replace it with your own."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from xtr_console import ConsoleStyle, ExitCode, as_command, escape
|
|
6
|
+
|
|
7
|
+
__all__ = ["hello"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@as_command("app:hello")
|
|
11
|
+
async def hello(io: ConsoleStyle, name: str = "world") -> int:
|
|
12
|
+
"""Greet someone.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
io: Where the command writes.
|
|
16
|
+
name: Who to greet.
|
|
17
|
+
"""
|
|
18
|
+
io.success(f"Hello, {escape(name)}!")
|
|
19
|
+
return ExitCode.SUCCESS
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""The kernel every entry point builds, and the environment it is built for.
|
|
2
|
+
|
|
3
|
+
``Kernel(...)`` stores a recipe and does no work; ``build()``, ``boot()`` and ``run()`` do,
|
|
4
|
+
each time afresh, so every entry point builds its own container from the same line.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
from xtr_dependency_injection import Kernel, exclude
|
|
13
|
+
from xtr_dotenv import Dotenv
|
|
14
|
+
|
|
15
|
+
__all__ = ["PROJECT_DIR", "kernel", "load_environment"]
|
|
16
|
+
|
|
17
|
+
PROJECT_DIR: Final = Path(__file__).resolve().parents[2]
|
|
18
|
+
"""The directory holding ``pyproject.toml`` and the ``.env`` files."""
|
|
19
|
+
|
|
20
|
+
kernel: Final = Kernel(
|
|
21
|
+
"${app}",
|
|
22
|
+
# The bundles are read from ${app}.bundles.BUNDLES; the environment from APP_ENV.
|
|
23
|
+
allowed_envs=("dev", "test", "prod"),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@exclude
|
|
28
|
+
def load_environment() -> None:
|
|
29
|
+
"""Load the layered ``.env`` files into the environment, before the kernel is built.
|
|
30
|
+
|
|
31
|
+
``@exclude`` keeps this helper out of the container: it is an entry point's concern,
|
|
32
|
+
never a service.
|
|
33
|
+
"""
|
|
34
|
+
_ = Dotenv().boot_env(str(PROJECT_DIR / ".env"))
|
|
File without changes
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "${name}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A console application on the xtr packages."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=${python}"
|
|
7
|
+
dependencies = []
|
|
8
|
+
|
|
9
|
+
[project.scripts]
|
|
10
|
+
${name} = "${app}.__main__:main"
|
|
11
|
+
|
|
12
|
+
[dependency-groups]
|
|
13
|
+
dev = []
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[tool.basedpyright]
|
|
20
|
+
typeCheckingMode = "all"
|
|
21
|
+
pythonVersion = "${python}"
|
|
22
|
+
include = ["src", "tests"]
|
|
23
|
+
exclude = ["**/__pycache__", "**/.venv", "var"]
|
|
24
|
+
reportUnusedCallResult = "warning"
|
|
25
|
+
|
|
26
|
+
[tool.ruff]
|
|
27
|
+
target-version = "py3${minor}"
|
|
28
|
+
line-length = 100
|
|
29
|
+
src = ["src", "tests"]
|
|
30
|
+
|
|
31
|
+
[tool.ruff.lint]
|
|
32
|
+
select = ["ALL"]
|
|
33
|
+
ignore = [
|
|
34
|
+
# Formatter conflicts.
|
|
35
|
+
"COM812",
|
|
36
|
+
"ISC001",
|
|
37
|
+
# Mutually exclusive docstring conventions; keep the modern one.
|
|
38
|
+
"D203",
|
|
39
|
+
"D213",
|
|
40
|
+
"CPY001",
|
|
41
|
+
# The container and the console read annotations at runtime: an import used only in
|
|
42
|
+
# an annotation is still needed at runtime, so none may move under TYPE_CHECKING.
|
|
43
|
+
"TC001",
|
|
44
|
+
"TC002",
|
|
45
|
+
"TC003",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[tool.ruff.lint.per-file-ignores]
|
|
49
|
+
"tests/**/*.py" = ["S101", "D"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint.pydocstyle]
|
|
52
|
+
convention = "google"
|
|
53
|
+
|
|
54
|
+
[tool.ty.src]
|
|
55
|
+
include = ["src", "tests"]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
testpaths = ["tests"]
|
|
59
|
+
addopts = ["-ra", "--strict-config", "--strict-markers"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
${python}
|
|
File without changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# A console application: the kernel, the console entry point and one command.
|
|
2
|
+
summary = "A console application: a kernel, its commands, the layered .env files and logging."
|
|
3
|
+
|
|
4
|
+
[dependencies]
|
|
5
|
+
packages = [
|
|
6
|
+
"xtr-dependency-injection",
|
|
7
|
+
"xtr-console[di]",
|
|
8
|
+
"xtr-dotenv[di,console]",
|
|
9
|
+
"xtr-logging[di]",
|
|
10
|
+
]
|
|
11
|
+
dev = ["xtr-recipes", "pytest", "ruff", "basedpyright", "ty"]
|
|
12
|
+
|
|
13
|
+
# Only what no package's recipe writes. The bundle list, .env and the ignore
|
|
14
|
+
# blocks are the recipes' to write, once the packages are installed.
|
|
15
|
+
[files]
|
|
16
|
+
"pyproject.toml" = "files/pyproject.toml.tmpl"
|
|
17
|
+
".python-version" = "files/python-version.tmpl"
|
|
18
|
+
".gitignore" = "files/gitignore.tmpl"
|
|
19
|
+
"README.md" = "files/README.md.tmpl"
|
|
20
|
+
"src/${app}/__init__.py" = "files/app/__init__.py.tmpl"
|
|
21
|
+
"src/${app}/__main__.py" = "files/app/__main__.py.tmpl"
|
|
22
|
+
"src/${app}/kernel.py" = "files/app/kernel.py.tmpl"
|
|
23
|
+
"src/${app}/py.typed" = "files/app/py.typed.tmpl"
|
|
24
|
+
"src/${app}/command/__init__.py" = "files/app/command/__init__.py.tmpl"
|
|
25
|
+
"src/${app}/command/hello_command.py" = "files/app/command/hello_command.py.tmpl"
|
|
26
|
+
"tests/__init__.py" = "files/tests/__init__.py.tmpl"
|
|
27
|
+
"tests/test_kernel.py" = "files/tests/test_kernel.py.tmpl"
|
|
28
|
+
|
|
29
|
+
[notes]
|
|
30
|
+
next = [
|
|
31
|
+
"uv run ${name} app:hello",
|
|
32
|
+
"uv run ${name} debug:bundles",
|
|
33
|
+
"uv run pytest",
|
|
34
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# ${name}
|
|
2
|
+
|
|
3
|
+
A web application on the xtr packages: FastAPI, served from the same kernel as its console.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uv run ${name}-web # serve on WEB_HOST:WEB_PORT, 127.0.0.1:8000 by default
|
|
9
|
+
uv run ${name} # every console command
|
|
10
|
+
uv run ${name} debug:router # the routes
|
|
11
|
+
uv run ${name} debug:bundles # the bundles and whether each is active
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The environment is read from `.env`, then `.env.local`, then `.env.<APP_ENV>` and
|
|
15
|
+
`.env.<APP_ENV>.local`; a variable set in the shell wins. Keep machine-specific values in
|
|
16
|
+
`.env.local`, which is not committed.
|
|
17
|
+
|
|
18
|
+
## Layout
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
src/${app}/
|
|
22
|
+
├── __main__.py the console entry point
|
|
23
|
+
├── kernel.py the kernel every entry point builds, and the environment loader
|
|
24
|
+
├── bundles.py the active bundles — written by xtr-recipes
|
|
25
|
+
├── command/ the console commands
|
|
26
|
+
└── web/
|
|
27
|
+
├── __main__.py the web entry point
|
|
28
|
+
├── app.py the FastAPI application, served from the kernel
|
|
29
|
+
└── routes.py the routes
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Add a package
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
uv run xtr-recipes recipes:add "xtr-messenger[di,console]"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
It installs the package and applies its recipe — the bundle, its configuration file, its
|
|
39
|
+
environment variables — and prints any step left to do by hand. `xtr.lock` records what was
|
|
40
|
+
applied; commit it.
|
|
41
|
+
|
|
42
|
+
## Check
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
uv run pytest
|
|
46
|
+
uv run ruff check && uv run ruff format --check
|
|
47
|
+
uv run basedpyright && uv run ty check
|
|
48
|
+
uv run xtr-recipes recipes:sync --check
|
|
49
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The console entry point: ``uv run ${name} <command>`` or ``python -m ${app} <command>``.
|
|
2
|
+
|
|
3
|
+
``*.__main__`` modules are never scanned by the kernel, so importing the application never
|
|
4
|
+
runs it.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from xtr_console.bundle import console
|
|
10
|
+
|
|
11
|
+
from ${app}.kernel import kernel, load_environment
|
|
12
|
+
|
|
13
|
+
__all__ = ["main"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
"""Load the environment, then boot, run the console, shut down and exit with its code."""
|
|
18
|
+
load_environment()
|
|
19
|
+
raise SystemExit(kernel.run(console))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""``app:hello``: the example command — replace it with your own."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from xtr_console import ConsoleStyle, ExitCode, as_command, escape
|
|
6
|
+
|
|
7
|
+
__all__ = ["hello"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@as_command("app:hello")
|
|
11
|
+
async def hello(io: ConsoleStyle, name: str = "world") -> int:
|
|
12
|
+
"""Greet someone.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
io: Where the command writes.
|
|
16
|
+
name: Who to greet.
|
|
17
|
+
"""
|
|
18
|
+
io.success(f"Hello, {escape(name)}!")
|
|
19
|
+
return ExitCode.SUCCESS
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""The request lifecycle: which application the router commands read.
|
|
2
|
+
|
|
3
|
+
Everything else is the bundle's default. ``app`` is the one thing only the application knows:
|
|
4
|
+
``${name} debug:router`` and ``${name} router:match PATH`` import it from here, so neither
|
|
5
|
+
needs ``--app``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from xtr_dependency_injection import configure
|
|
11
|
+
from xtr_http_kernel.bundle import HttpKernelConfig
|
|
12
|
+
|
|
13
|
+
__all__ = ["http_kernel"]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@configure
|
|
17
|
+
def http_kernel() -> HttpKernelConfig:
|
|
18
|
+
"""Name the application the console reports on; nothing is imported until it does."""
|
|
19
|
+
return HttpKernelConfig(app="${app}.web.app:app")
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""The kernel every entry point builds, and the environment it is built for.
|
|
2
|
+
|
|
3
|
+
``Kernel(...)`` stores a recipe and does no work; ``build()``, ``boot()`` and ``run()`` do,
|
|
4
|
+
each time afresh, so every entry point builds its own container from the same line.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Final
|
|
11
|
+
|
|
12
|
+
from xtr_dependency_injection import DEFAULT_EXCLUDES, Kernel, exclude
|
|
13
|
+
from xtr_dotenv import Dotenv
|
|
14
|
+
|
|
15
|
+
__all__ = ["PROJECT_DIR", "kernel", "load_environment"]
|
|
16
|
+
|
|
17
|
+
PROJECT_DIR: Final = Path(__file__).resolve().parents[2]
|
|
18
|
+
"""The directory holding ``pyproject.toml`` and the ``.env`` files."""
|
|
19
|
+
|
|
20
|
+
kernel: Final = Kernel(
|
|
21
|
+
"${app}",
|
|
22
|
+
# The bundles are read from ${app}.bundles.BUNDLES; the environment from APP_ENV.
|
|
23
|
+
allowed_envs=("dev", "test", "prod"),
|
|
24
|
+
# ${app}.web.app builds the web application as it is imported, and that application is
|
|
25
|
+
# served from this very kernel: scanning it would import it while the kernel builds.
|
|
26
|
+
exclude=(*DEFAULT_EXCLUDES, "${app}.web.app"),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@exclude
|
|
31
|
+
def load_environment() -> None:
|
|
32
|
+
"""Load the layered ``.env`` files into the environment, before the kernel is built.
|
|
33
|
+
|
|
34
|
+
``@exclude`` keeps this helper out of the container: it is an entry point's concern,
|
|
35
|
+
never a service.
|
|
36
|
+
"""
|
|
37
|
+
_ = Dotenv().boot_env(str(PROJECT_DIR / ".env"))
|
|
File without changes
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""The web entry point: ``uv run ${name}-web`` or ``python -m ${app}.web``.
|
|
2
|
+
|
|
3
|
+
The environment is loaded before anything reads it, then the server runs the application on
|
|
4
|
+
``WEB_HOST`` / ``WEB_PORT``, restarting it on every source change when ``WEB_RELOAD=1`` —
|
|
5
|
+
``xtr serve --reload`` sets it. Ctrl-C or SIGTERM stops it: the application's lifespan ends and
|
|
6
|
+
the kernel shuts down with it.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
from typing import Final
|
|
13
|
+
|
|
14
|
+
import uvicorn
|
|
15
|
+
|
|
16
|
+
from ${app}.kernel import PROJECT_DIR, load_environment
|
|
17
|
+
|
|
18
|
+
__all__ = ["main"]
|
|
19
|
+
|
|
20
|
+
_HOST: Final = "127.0.0.1"
|
|
21
|
+
_PORT: Final = 8000
|
|
22
|
+
# An import string, not the object: reloading imports the application afresh in a new process,
|
|
23
|
+
# which inherits the environment loaded here.
|
|
24
|
+
_APP: Final = "${app}.web.app:app"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> None:
|
|
28
|
+
"""Load the environment, then serve the application where the variables say."""
|
|
29
|
+
load_environment()
|
|
30
|
+
uvicorn.run(
|
|
31
|
+
_APP,
|
|
32
|
+
host=os.environ.get("WEB_HOST", _HOST),
|
|
33
|
+
port=int(os.environ.get("WEB_PORT", str(_PORT))),
|
|
34
|
+
reload=os.environ.get("WEB_RELOAD") == "1",
|
|
35
|
+
# The application's own sources, not the whole project: the virtual environment
|
|
36
|
+
# under it would only slow every scan down.
|
|
37
|
+
reload_dirs=[str(PROJECT_DIR / "src")],
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
main()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The web application: FastAPI, as the framework documents it, served from the kernel.
|
|
2
|
+
|
|
3
|
+
``setup(app, kernel)`` is the whole of serving it from the kernel: every life of the
|
|
4
|
+
application builds and boots its own kernel and shuts it down again, and every request runs
|
|
5
|
+
through the lifecycle the http_kernel bundle contributes. Left out of the kernel's scan
|
|
6
|
+
(see ``exclude`` in :mod:`${app}.kernel`): importing this module builds the application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from fastapi import FastAPI
|
|
12
|
+
from xtr_http_kernel import setup
|
|
13
|
+
|
|
14
|
+
from ${app}.kernel import kernel
|
|
15
|
+
|
|
16
|
+
from .routes import router
|
|
17
|
+
|
|
18
|
+
__all__ = ["app"]
|
|
19
|
+
|
|
20
|
+
app = FastAPI(title="${name}")
|
|
21
|
+
app.include_router(router)
|
|
22
|
+
|
|
23
|
+
setup(app, kernel)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""The routes — replace the example with your own."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
__all__ = ["router"]
|
|
8
|
+
|
|
9
|
+
router = APIRouter()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@router.get("/")
|
|
13
|
+
async def home() -> dict[str, str]:
|
|
14
|
+
"""Answer that the application is up."""
|
|
15
|
+
return {"status": "ok"}
|