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.
Files changed (73) hide show
  1. xtr_cli/__init__.py +79 -0
  2. xtr_cli/__main__.py +28 -0
  3. xtr_cli/add_on.py +52 -0
  4. xtr_cli/add_on_catalog.py +167 -0
  5. xtr_cli/add_on_choice.py +40 -0
  6. xtr_cli/command/__init__.py +14 -0
  7. xtr_cli/command/init_command.py +69 -0
  8. xtr_cli/command/new_command.py +72 -0
  9. xtr_cli/command/serve_command.py +114 -0
  10. xtr_cli/command_support.py +189 -0
  11. xtr_cli/exception/__init__.py +34 -0
  12. xtr_cli/exception/ambiguous_web_entry_point_error.py +27 -0
  13. xtr_cli/exception/cli_error.py +14 -0
  14. xtr_cli/exception/directory_not_empty_error.py +35 -0
  15. xtr_cli/exception/invalid_project_name_error.py +33 -0
  16. xtr_cli/exception/invalid_template_error.py +28 -0
  17. xtr_cli/exception/project_not_found_error.py +28 -0
  18. xtr_cli/exception/step_failed_error.py +38 -0
  19. xtr_cli/exception/unknown_add_on_error.py +28 -0
  20. xtr_cli/exception/unknown_template_error.py +29 -0
  21. xtr_cli/exception/unsupported_python_error.py +28 -0
  22. xtr_cli/exception/web_entry_point_not_found_error.py +31 -0
  23. xtr_cli/process_runner_interface.py +29 -0
  24. xtr_cli/project_name.py +55 -0
  25. xtr_cli/project_questions.py +108 -0
  26. xtr_cli/project_settings.py +89 -0
  27. xtr_cli/py.typed +0 -0
  28. xtr_cli/scaffold_plan.py +41 -0
  29. xtr_cli/scaffolder.py +159 -0
  30. xtr_cli/subprocess_runner.py +47 -0
  31. xtr_cli/template.py +178 -0
  32. xtr_cli/template_catalog.py +62 -0
  33. xtr_cli/templates/__init__.py +10 -0
  34. xtr_cli/templates/cli/files/README.md.tmpl +44 -0
  35. xtr_cli/templates/cli/files/app/__init__.py.tmpl +5 -0
  36. xtr_cli/templates/cli/files/app/__main__.py.tmpl +23 -0
  37. xtr_cli/templates/cli/files/app/command/__init__.py.tmpl +5 -0
  38. xtr_cli/templates/cli/files/app/command/hello_command.py.tmpl +19 -0
  39. xtr_cli/templates/cli/files/app/kernel.py.tmpl +34 -0
  40. xtr_cli/templates/cli/files/app/py.typed.tmpl +0 -0
  41. xtr_cli/templates/cli/files/gitignore.tmpl +14 -0
  42. xtr_cli/templates/cli/files/pyproject.toml.tmpl +59 -0
  43. xtr_cli/templates/cli/files/python-version.tmpl +1 -0
  44. xtr_cli/templates/cli/files/tests/__init__.py.tmpl +0 -0
  45. xtr_cli/templates/cli/files/tests/test_kernel.py.tmpl +9 -0
  46. xtr_cli/templates/cli/template.toml +34 -0
  47. xtr_cli/templates/webapp/files/README.md.tmpl +49 -0
  48. xtr_cli/templates/webapp/files/app/__init__.py.tmpl +5 -0
  49. xtr_cli/templates/webapp/files/app/__main__.py.tmpl +23 -0
  50. xtr_cli/templates/webapp/files/app/command/__init__.py.tmpl +5 -0
  51. xtr_cli/templates/webapp/files/app/command/hello_command.py.tmpl +19 -0
  52. xtr_cli/templates/webapp/files/app/config/__init__.py.tmpl +5 -0
  53. xtr_cli/templates/webapp/files/app/config/http_kernel.py.tmpl +19 -0
  54. xtr_cli/templates/webapp/files/app/kernel.py.tmpl +37 -0
  55. xtr_cli/templates/webapp/files/app/py.typed.tmpl +0 -0
  56. xtr_cli/templates/webapp/files/app/web/__init__.py.tmpl +5 -0
  57. xtr_cli/templates/webapp/files/app/web/__main__.py.tmpl +42 -0
  58. xtr_cli/templates/webapp/files/app/web/app.py.tmpl +23 -0
  59. xtr_cli/templates/webapp/files/app/web/routes.py.tmpl +15 -0
  60. xtr_cli/templates/webapp/files/gitignore.tmpl +14 -0
  61. xtr_cli/templates/webapp/files/pyproject.toml.tmpl +60 -0
  62. xtr_cli/templates/webapp/files/python-version.tmpl +1 -0
  63. xtr_cli/templates/webapp/files/tests/__init__.py.tmpl +0 -0
  64. xtr_cli/templates/webapp/files/tests/test_kernel.py.tmpl +9 -0
  65. xtr_cli/templates/webapp/files/tests/test_web.py.tmpl +28 -0
  66. xtr_cli/templates/webapp/template.toml +45 -0
  67. xtr_cli/web_entry_point.py +76 -0
  68. xtr_cli/web_entry_point_finder.py +194 -0
  69. xtr_cli-4.0.2.dist-info/METADATA +307 -0
  70. xtr_cli-4.0.2.dist-info/RECORD +73 -0
  71. xtr_cli-4.0.2.dist-info/WHEEL +4 -0
  72. xtr_cli-4.0.2.dist-info/entry_points.txt +4 -0
  73. 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,5 @@
1
+ """${name}: a console application on the xtr packages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []
@@ -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,5 @@
1
+ """The console commands: every module here is scanned, and each ``@as_command`` registered."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []
@@ -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,14 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ /.venv/
5
+ /dist/
6
+ /build/
7
+
8
+ # Tools
9
+ /.pytest_cache/
10
+ /.ruff_cache/
11
+ /.coverage
12
+
13
+ # What the application writes while it runs
14
+ /var/
@@ -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,9 @@
1
+ """The application's kernel builds, with every listed bundle."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ${app}.kernel import kernel
6
+
7
+
8
+ def test_the_kernel_builds_in_the_test_environment() -> None:
9
+ _ = kernel.with_env("test").build()
@@ -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,5 @@
1
+ """${name}: a web application on the xtr packages."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []
@@ -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,5 @@
1
+ """The console commands: every module here is scanned, and each ``@as_command`` registered."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []
@@ -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,5 @@
1
+ """The application's bundle configuration: one ``@configure`` function per bundle it changes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []
@@ -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,5 @@
1
+ """The web side of the application: the FastAPI application and its routes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: list[str] = []
@@ -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"}
@@ -0,0 +1,14 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ /.venv/
5
+ /dist/
6
+ /build/
7
+
8
+ # Tools
9
+ /.pytest_cache/
10
+ /.ruff_cache/
11
+ /.coverage
12
+
13
+ # What the application writes while it runs
14
+ /var/