avalon-cli 0.2.0__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.
- avalon_cli-0.2.0/LICENSE +21 -0
- avalon_cli-0.2.0/PKG-INFO +192 -0
- avalon_cli-0.2.0/README.md +167 -0
- avalon_cli-0.2.0/pyproject.toml +39 -0
- avalon_cli-0.2.0/setup.cfg +4 -0
- avalon_cli-0.2.0/src/avalon_cli/__init__.py +37 -0
- avalon_cli-0.2.0/src/avalon_cli/__main__.py +7 -0
- avalon_cli-0.2.0/src/avalon_cli/cli.py +163 -0
- avalon_cli-0.2.0/src/avalon_cli/config.py +128 -0
- avalon_cli-0.2.0/src/avalon_cli/py.typed +0 -0
- avalon_cli-0.2.0/src/avalon_cli/reloader.py +218 -0
- avalon_cli-0.2.0/src/avalon_cli/scaffold.py +89 -0
- avalon_cli-0.2.0/src/avalon_cli/templates.py +235 -0
- avalon_cli-0.2.0/src/avalon_cli.egg-info/PKG-INFO +192 -0
- avalon_cli-0.2.0/src/avalon_cli.egg-info/SOURCES.txt +21 -0
- avalon_cli-0.2.0/src/avalon_cli.egg-info/dependency_links.txt +1 -0
- avalon_cli-0.2.0/src/avalon_cli.egg-info/entry_points.txt +2 -0
- avalon_cli-0.2.0/src/avalon_cli.egg-info/top_level.txt +1 -0
- avalon_cli-0.2.0/tests/test_cli.py +117 -0
- avalon_cli-0.2.0/tests/test_config.py +73 -0
- avalon_cli-0.2.0/tests/test_reloader.py +158 -0
- avalon_cli-0.2.0/tests/test_scaffold.py +86 -0
- avalon_cli-0.2.0/tests/test_templates.py +39 -0
avalon_cli-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nehz
|
|
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.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: avalon-cli
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Command-line companion for the Avalon real-time web framework: scaffold projects and run an auto-reloading dev server.
|
|
5
|
+
Author: nehz
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: avalon,cli,scaffold,dev-server,real-time,web
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Environment :: Web Environment
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
19
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
|
|
26
|
+
# avalon-cli
|
|
27
|
+
|
|
28
|
+
**The command-line companion for the [Avalon](https://pypi.org/project/avalon/) real-time web framework.**
|
|
29
|
+
|
|
30
|
+
`avalon-cli` gets you from zero to a running, auto-reloading app in two commands:
|
|
31
|
+
`avalon new` scaffolds a project and `avalon dev` runs it, restarting the server
|
|
32
|
+
every time you save a file. It uses only the Python standard library and does not
|
|
33
|
+
import `avalon` itself, so it installs in seconds and works with any Avalon version
|
|
34
|
+
(or with no framework at all, via the `static` template).
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Project scaffolding**: `avalon new` generates a ready-to-run project from a built-in template.
|
|
39
|
+
- **Auto-reloading dev server**: `avalon dev` runs your dev command, watches files by polling
|
|
40
|
+
(no native dependencies), and restarts the process on change. If the process crashes it waits
|
|
41
|
+
for your fix and starts again on the next save, so you never have to restart `avalon dev`.
|
|
42
|
+
- **One config file**: `avalon.toml` holds the dev command, host, port and watch rules, found by
|
|
43
|
+
walking upward from the current directory.
|
|
44
|
+
- **Clean shutdown**: Ctrl-C or SIGTERM always terminates the child process; nothing is orphaned.
|
|
45
|
+
- **Zero dependencies**: standard library only, Python 3.11+.
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install avalon-cli
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
This installs the `avalon` command. `python -m avalon_cli` works too.
|
|
54
|
+
|
|
55
|
+
## Quickstart
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
avalon new chat-demo # Avalon app (default "minimal" template)
|
|
59
|
+
cd chat-demo
|
|
60
|
+
pip install avalon # the framework the generated app.py uses
|
|
61
|
+
avalon dev # http://127.0.0.1:8000/, reloads on save
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
No framework yet? The `static` template needs nothing but Python:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
avalon new mysite --template static
|
|
68
|
+
avalon dev -C mysite --port 9000
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Commands
|
|
72
|
+
|
|
73
|
+
All commands print errors as `error: <message>` on stderr and exit with status 1;
|
|
74
|
+
usage errors exit with status 2.
|
|
75
|
+
|
|
76
|
+
### `avalon new NAME [-t TEMPLATE] [-d DIRECTORY] [--force]`
|
|
77
|
+
|
|
78
|
+
Create the directory `DIRECTORY/NAME` and fill it from a template.
|
|
79
|
+
|
|
80
|
+
| Option | Default | Meaning |
|
|
81
|
+
| --- | --- | --- |
|
|
82
|
+
| `NAME` | (required) | Project name and directory name. Must start with a letter and contain only letters, digits, `-` and `_`. |
|
|
83
|
+
| `-t`, `--template` | `minimal` | One of the templates listed by `avalon templates`. |
|
|
84
|
+
| `-d`, `--directory` | `.` | Parent directory to create the project in. |
|
|
85
|
+
| `--force` | off | Write into an existing non-empty directory, overwriting files the template provides (other files are left alone). |
|
|
86
|
+
|
|
87
|
+
An existing *empty* directory is always accepted.
|
|
88
|
+
|
|
89
|
+
### `avalon templates`
|
|
90
|
+
|
|
91
|
+
List the built-in templates:
|
|
92
|
+
|
|
93
|
+
| Template | Files | Notes |
|
|
94
|
+
| --- | --- | --- |
|
|
95
|
+
| `minimal` (default) | `avalon.toml`, `app.py`, `templates/index.html`, `README.md`, `.gitignore` | An Avalon app with one page and a WebSocket echo endpoint. Requires `avalon`. |
|
|
96
|
+
| `static` | `avalon.toml`, `public/index.html`, `public/style.css`, `public/app.js`, `README.md`, `.gitignore` | Served by `python -m http.server`; no dependencies. |
|
|
97
|
+
|
|
98
|
+
### `avalon dev [-C PROJECT] [--host HOST] [-p PORT] [--interval SECONDS] [--no-reload]`
|
|
99
|
+
|
|
100
|
+
Find `avalon.toml` (in `PROJECT` or any parent directory), start the configured dev
|
|
101
|
+
command from the project root, and restart it whenever a watched file is added,
|
|
102
|
+
modified or removed.
|
|
103
|
+
|
|
104
|
+
| Option | Default | Meaning |
|
|
105
|
+
| --- | --- | --- |
|
|
106
|
+
| `-C`, `--project` | `.` | Directory to start looking for `avalon.toml`. |
|
|
107
|
+
| `--host` | `[dev].host` | Overrides the host. |
|
|
108
|
+
| `-p`, `--port` | `[dev].port` | Overrides the port (1-65535). |
|
|
109
|
+
| `--interval` | `[dev].interval` | Seconds between file-change polls. |
|
|
110
|
+
| `--no-reload` | off | Run the command once, without watching; `avalon dev` exits with the command's exit code. |
|
|
111
|
+
|
|
112
|
+
The child process receives `AVALON_HOST`, `AVALON_PORT` and `AVALON_ENV=development`
|
|
113
|
+
in its environment. With reloading on, `avalon dev` exits with status 0 on SIGTERM
|
|
114
|
+
and 130 on Ctrl-C.
|
|
115
|
+
|
|
116
|
+
### `avalon info [-C PROJECT]`
|
|
117
|
+
|
|
118
|
+
Print the `avalon-cli` and Python versions and, if a project is found, its name,
|
|
119
|
+
root, fully-resolved dev command, address, watch paths and extensions.
|
|
120
|
+
|
|
121
|
+
### `avalon --version`
|
|
122
|
+
|
|
123
|
+
Print `avalon 0.2.0`.
|
|
124
|
+
|
|
125
|
+
## Configuration: `avalon.toml`
|
|
126
|
+
|
|
127
|
+
```toml
|
|
128
|
+
[project]
|
|
129
|
+
name = "chat-demo" # default: the directory name
|
|
130
|
+
|
|
131
|
+
[dev]
|
|
132
|
+
command = "{python} app.py" # placeholders: {host}, {port}, {python}
|
|
133
|
+
host = "127.0.0.1"
|
|
134
|
+
port = 8000
|
|
135
|
+
watch = ["."] # files or directories, relative to the project root
|
|
136
|
+
extensions = [".py", ".html", ".css", ".js", ".toml"] # [] watches every file
|
|
137
|
+
ignore = [".venv", ".git", "__pycache__", "node_modules"] # glob patterns matched against names
|
|
138
|
+
interval = 0.5 # seconds between polls
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Every key is optional; the values above are the defaults. Extensions without a
|
|
142
|
+
leading dot get one (`"py"` becomes `".py"`). Unknown keys in `[dev]` are rejected
|
|
143
|
+
so typos are caught early. The command is split shell-style *before* placeholders
|
|
144
|
+
are substituted, so a `{python}` path containing spaces stays a single argument.
|
|
145
|
+
`{python}` is the interpreter running `avalon-cli`.
|
|
146
|
+
|
|
147
|
+
## Python API
|
|
148
|
+
|
|
149
|
+
Everything the CLI does is available from `avalon_cli`:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from avalon_cli import DevServer, build_command, create_project, load_config
|
|
153
|
+
|
|
154
|
+
result = create_project("chat-demo", template="minimal", parent=".", force=False)
|
|
155
|
+
print(result.root, result.template, result.files) # ScaffoldResult
|
|
156
|
+
|
|
157
|
+
config = load_config(result.root) # ProjectConfig(root, name, dev=DevConfig(...))
|
|
158
|
+
argv = build_command(config.dev.command, host=config.dev.host, port=config.dev.port)
|
|
159
|
+
|
|
160
|
+
server = DevServer(
|
|
161
|
+
argv,
|
|
162
|
+
cwd=config.root,
|
|
163
|
+
watch=config.dev.watch,
|
|
164
|
+
extensions=config.dev.extensions,
|
|
165
|
+
ignore=config.dev.ignore,
|
|
166
|
+
interval=config.dev.interval,
|
|
167
|
+
)
|
|
168
|
+
server.run() # blocks; pass a threading.Event to stop it from another thread
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Name | Description |
|
|
172
|
+
| --- | --- |
|
|
173
|
+
| `create_project(name, template="minimal", parent=".", *, force=False) -> ScaffoldResult` | Scaffold a project; raises `ScaffoldError`. |
|
|
174
|
+
| `load_config(start=".") -> ProjectConfig` | Locate and validate `avalon.toml`; raises `ConfigError`. |
|
|
175
|
+
| `find_project_root(start=".") -> Path` | Directory containing the nearest `avalon.toml`; raises `ConfigError`. |
|
|
176
|
+
| `build_command(template, *, host, port, python=None) -> list[str]` | Turn a dev command template into argv; raises `ValueError`. |
|
|
177
|
+
| `DevServer(argv, *, cwd=".", env=None, watch=(".",), extensions=(), ignore=(), interval=0.5, reload=True, log=...)` | Reloading process runner with `start()`, `stop(timeout=5.0)`, `restart()`, `poll_changes()`, `run(stop_event=None) -> int`, and a `restarts` counter. |
|
|
178
|
+
| `take_snapshot(roots, extensions=(), ignore=()) -> dict[Path, int]` | File-to-mtime map of watched files. |
|
|
179
|
+
| `diff_snapshots(old, new) -> set[Path]` | Paths added, removed or modified between two snapshots. |
|
|
180
|
+
| `TEMPLATES`, `get_template(name)`, `ProjectTemplate` | The built-in template registry. |
|
|
181
|
+
|
|
182
|
+
## Development
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
186
|
+
pip install -e .
|
|
187
|
+
python -m unittest discover -s tests -t .
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
MIT
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# avalon-cli
|
|
2
|
+
|
|
3
|
+
**The command-line companion for the [Avalon](https://pypi.org/project/avalon/) real-time web framework.**
|
|
4
|
+
|
|
5
|
+
`avalon-cli` gets you from zero to a running, auto-reloading app in two commands:
|
|
6
|
+
`avalon new` scaffolds a project and `avalon dev` runs it, restarting the server
|
|
7
|
+
every time you save a file. It uses only the Python standard library and does not
|
|
8
|
+
import `avalon` itself, so it installs in seconds and works with any Avalon version
|
|
9
|
+
(or with no framework at all, via the `static` template).
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Project scaffolding**: `avalon new` generates a ready-to-run project from a built-in template.
|
|
14
|
+
- **Auto-reloading dev server**: `avalon dev` runs your dev command, watches files by polling
|
|
15
|
+
(no native dependencies), and restarts the process on change. If the process crashes it waits
|
|
16
|
+
for your fix and starts again on the next save, so you never have to restart `avalon dev`.
|
|
17
|
+
- **One config file**: `avalon.toml` holds the dev command, host, port and watch rules, found by
|
|
18
|
+
walking upward from the current directory.
|
|
19
|
+
- **Clean shutdown**: Ctrl-C or SIGTERM always terminates the child process; nothing is orphaned.
|
|
20
|
+
- **Zero dependencies**: standard library only, Python 3.11+.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install avalon-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This installs the `avalon` command. `python -m avalon_cli` works too.
|
|
29
|
+
|
|
30
|
+
## Quickstart
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
avalon new chat-demo # Avalon app (default "minimal" template)
|
|
34
|
+
cd chat-demo
|
|
35
|
+
pip install avalon # the framework the generated app.py uses
|
|
36
|
+
avalon dev # http://127.0.0.1:8000/, reloads on save
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
No framework yet? The `static` template needs nothing but Python:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
avalon new mysite --template static
|
|
43
|
+
avalon dev -C mysite --port 9000
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Commands
|
|
47
|
+
|
|
48
|
+
All commands print errors as `error: <message>` on stderr and exit with status 1;
|
|
49
|
+
usage errors exit with status 2.
|
|
50
|
+
|
|
51
|
+
### `avalon new NAME [-t TEMPLATE] [-d DIRECTORY] [--force]`
|
|
52
|
+
|
|
53
|
+
Create the directory `DIRECTORY/NAME` and fill it from a template.
|
|
54
|
+
|
|
55
|
+
| Option | Default | Meaning |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| `NAME` | (required) | Project name and directory name. Must start with a letter and contain only letters, digits, `-` and `_`. |
|
|
58
|
+
| `-t`, `--template` | `minimal` | One of the templates listed by `avalon templates`. |
|
|
59
|
+
| `-d`, `--directory` | `.` | Parent directory to create the project in. |
|
|
60
|
+
| `--force` | off | Write into an existing non-empty directory, overwriting files the template provides (other files are left alone). |
|
|
61
|
+
|
|
62
|
+
An existing *empty* directory is always accepted.
|
|
63
|
+
|
|
64
|
+
### `avalon templates`
|
|
65
|
+
|
|
66
|
+
List the built-in templates:
|
|
67
|
+
|
|
68
|
+
| Template | Files | Notes |
|
|
69
|
+
| --- | --- | --- |
|
|
70
|
+
| `minimal` (default) | `avalon.toml`, `app.py`, `templates/index.html`, `README.md`, `.gitignore` | An Avalon app with one page and a WebSocket echo endpoint. Requires `avalon`. |
|
|
71
|
+
| `static` | `avalon.toml`, `public/index.html`, `public/style.css`, `public/app.js`, `README.md`, `.gitignore` | Served by `python -m http.server`; no dependencies. |
|
|
72
|
+
|
|
73
|
+
### `avalon dev [-C PROJECT] [--host HOST] [-p PORT] [--interval SECONDS] [--no-reload]`
|
|
74
|
+
|
|
75
|
+
Find `avalon.toml` (in `PROJECT` or any parent directory), start the configured dev
|
|
76
|
+
command from the project root, and restart it whenever a watched file is added,
|
|
77
|
+
modified or removed.
|
|
78
|
+
|
|
79
|
+
| Option | Default | Meaning |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `-C`, `--project` | `.` | Directory to start looking for `avalon.toml`. |
|
|
82
|
+
| `--host` | `[dev].host` | Overrides the host. |
|
|
83
|
+
| `-p`, `--port` | `[dev].port` | Overrides the port (1-65535). |
|
|
84
|
+
| `--interval` | `[dev].interval` | Seconds between file-change polls. |
|
|
85
|
+
| `--no-reload` | off | Run the command once, without watching; `avalon dev` exits with the command's exit code. |
|
|
86
|
+
|
|
87
|
+
The child process receives `AVALON_HOST`, `AVALON_PORT` and `AVALON_ENV=development`
|
|
88
|
+
in its environment. With reloading on, `avalon dev` exits with status 0 on SIGTERM
|
|
89
|
+
and 130 on Ctrl-C.
|
|
90
|
+
|
|
91
|
+
### `avalon info [-C PROJECT]`
|
|
92
|
+
|
|
93
|
+
Print the `avalon-cli` and Python versions and, if a project is found, its name,
|
|
94
|
+
root, fully-resolved dev command, address, watch paths and extensions.
|
|
95
|
+
|
|
96
|
+
### `avalon --version`
|
|
97
|
+
|
|
98
|
+
Print `avalon 0.2.0`.
|
|
99
|
+
|
|
100
|
+
## Configuration: `avalon.toml`
|
|
101
|
+
|
|
102
|
+
```toml
|
|
103
|
+
[project]
|
|
104
|
+
name = "chat-demo" # default: the directory name
|
|
105
|
+
|
|
106
|
+
[dev]
|
|
107
|
+
command = "{python} app.py" # placeholders: {host}, {port}, {python}
|
|
108
|
+
host = "127.0.0.1"
|
|
109
|
+
port = 8000
|
|
110
|
+
watch = ["."] # files or directories, relative to the project root
|
|
111
|
+
extensions = [".py", ".html", ".css", ".js", ".toml"] # [] watches every file
|
|
112
|
+
ignore = [".venv", ".git", "__pycache__", "node_modules"] # glob patterns matched against names
|
|
113
|
+
interval = 0.5 # seconds between polls
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Every key is optional; the values above are the defaults. Extensions without a
|
|
117
|
+
leading dot get one (`"py"` becomes `".py"`). Unknown keys in `[dev]` are rejected
|
|
118
|
+
so typos are caught early. The command is split shell-style *before* placeholders
|
|
119
|
+
are substituted, so a `{python}` path containing spaces stays a single argument.
|
|
120
|
+
`{python}` is the interpreter running `avalon-cli`.
|
|
121
|
+
|
|
122
|
+
## Python API
|
|
123
|
+
|
|
124
|
+
Everything the CLI does is available from `avalon_cli`:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from avalon_cli import DevServer, build_command, create_project, load_config
|
|
128
|
+
|
|
129
|
+
result = create_project("chat-demo", template="minimal", parent=".", force=False)
|
|
130
|
+
print(result.root, result.template, result.files) # ScaffoldResult
|
|
131
|
+
|
|
132
|
+
config = load_config(result.root) # ProjectConfig(root, name, dev=DevConfig(...))
|
|
133
|
+
argv = build_command(config.dev.command, host=config.dev.host, port=config.dev.port)
|
|
134
|
+
|
|
135
|
+
server = DevServer(
|
|
136
|
+
argv,
|
|
137
|
+
cwd=config.root,
|
|
138
|
+
watch=config.dev.watch,
|
|
139
|
+
extensions=config.dev.extensions,
|
|
140
|
+
ignore=config.dev.ignore,
|
|
141
|
+
interval=config.dev.interval,
|
|
142
|
+
)
|
|
143
|
+
server.run() # blocks; pass a threading.Event to stop it from another thread
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
| Name | Description |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `create_project(name, template="minimal", parent=".", *, force=False) -> ScaffoldResult` | Scaffold a project; raises `ScaffoldError`. |
|
|
149
|
+
| `load_config(start=".") -> ProjectConfig` | Locate and validate `avalon.toml`; raises `ConfigError`. |
|
|
150
|
+
| `find_project_root(start=".") -> Path` | Directory containing the nearest `avalon.toml`; raises `ConfigError`. |
|
|
151
|
+
| `build_command(template, *, host, port, python=None) -> list[str]` | Turn a dev command template into argv; raises `ValueError`. |
|
|
152
|
+
| `DevServer(argv, *, cwd=".", env=None, watch=(".",), extensions=(), ignore=(), interval=0.5, reload=True, log=...)` | Reloading process runner with `start()`, `stop(timeout=5.0)`, `restart()`, `poll_changes()`, `run(stop_event=None) -> int`, and a `restarts` counter. |
|
|
153
|
+
| `take_snapshot(roots, extensions=(), ignore=()) -> dict[Path, int]` | File-to-mtime map of watched files. |
|
|
154
|
+
| `diff_snapshots(old, new) -> set[Path]` | Paths added, removed or modified between two snapshots. |
|
|
155
|
+
| `TEMPLATES`, `get_template(name)`, `ProjectTemplate` | The built-in template registry. |
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
python3 -m venv .venv && . .venv/bin/activate
|
|
161
|
+
pip install -e .
|
|
162
|
+
python -m unittest discover -s tests -t .
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "avalon-cli"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "Command-line companion for the Avalon real-time web framework: scaffold projects and run an auto-reloading dev server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "nehz" }]
|
|
14
|
+
keywords = ["avalon", "cli", "scaffold", "dev-server", "real-time", "web"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Environment :: Web Environment",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
27
|
+
"Topic :: Software Development :: Code Generators",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
avalon = "avalon_cli.cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.packages.find]
|
|
36
|
+
where = ["src"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-data]
|
|
39
|
+
avalon_cli = ["py.typed"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""avalon-cli: command-line companion for the Avalon real-time web framework.
|
|
2
|
+
|
|
3
|
+
The package is self-contained (standard library only) and does not import
|
|
4
|
+
``avalon`` itself. The public API mirrors the CLI commands:
|
|
5
|
+
|
|
6
|
+
* :func:`create_project` - ``avalon new``
|
|
7
|
+
* :class:`DevServer` / :func:`build_command` - ``avalon dev``
|
|
8
|
+
* :func:`load_config` - reads ``avalon.toml``
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
__version__ = "0.2.0"
|
|
14
|
+
|
|
15
|
+
from .config import ConfigError, DevConfig, ProjectConfig, find_project_root, load_config # noqa: E402
|
|
16
|
+
from .reloader import DevServer, build_command, diff_snapshots, take_snapshot # noqa: E402
|
|
17
|
+
from .scaffold import ScaffoldError, ScaffoldResult, create_project # noqa: E402
|
|
18
|
+
from .templates import TEMPLATES, ProjectTemplate, get_template # noqa: E402
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"__version__",
|
|
22
|
+
"ConfigError",
|
|
23
|
+
"DevConfig",
|
|
24
|
+
"DevServer",
|
|
25
|
+
"ProjectConfig",
|
|
26
|
+
"ProjectTemplate",
|
|
27
|
+
"ScaffoldError",
|
|
28
|
+
"ScaffoldResult",
|
|
29
|
+
"TEMPLATES",
|
|
30
|
+
"build_command",
|
|
31
|
+
"create_project",
|
|
32
|
+
"diff_snapshots",
|
|
33
|
+
"find_project_root",
|
|
34
|
+
"get_template",
|
|
35
|
+
"load_config",
|
|
36
|
+
"take_snapshot",
|
|
37
|
+
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""The ``avalon`` command-line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import os
|
|
7
|
+
import platform
|
|
8
|
+
import shlex
|
|
9
|
+
import signal
|
|
10
|
+
import sys
|
|
11
|
+
import threading
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
from typing import TextIO
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
from .config import ConfigError, load_config
|
|
17
|
+
from .reloader import DevServer, build_command
|
|
18
|
+
from .scaffold import ScaffoldError, create_project
|
|
19
|
+
from .templates import DEFAULT_TEMPLATE, TEMPLATES
|
|
20
|
+
|
|
21
|
+
__all__ = ["build_parser", "main"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CLIError(Exception):
|
|
25
|
+
"""An error reported to the user as ``error: <message>`` with exit code 1."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _cmd_new(args: argparse.Namespace, out: TextIO) -> int:
|
|
29
|
+
result = create_project(args.name, args.template, args.directory, force=args.force)
|
|
30
|
+
out.write(f"Created {result.template!r} project in {result.root}\n")
|
|
31
|
+
for path in result.files:
|
|
32
|
+
out.write(f" {path.relative_to(result.root).as_posix()}\n")
|
|
33
|
+
out.write(f"\nNext steps:\n cd {shlex.quote(str(result.root))}\n avalon dev\n")
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cmd_templates(args: argparse.Namespace, out: TextIO) -> int:
|
|
38
|
+
width = max(len(name) for name in TEMPLATES)
|
|
39
|
+
for name in sorted(TEMPLATES):
|
|
40
|
+
marker = " (default)" if name == DEFAULT_TEMPLATE else ""
|
|
41
|
+
out.write(f"{name:<{width}} {TEMPLATES[name].description}{marker}\n")
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _cmd_info(args: argparse.Namespace, out: TextIO) -> int:
|
|
46
|
+
out.write(f"avalon-cli {__version__}\n")
|
|
47
|
+
out.write(f"python {platform.python_version()} ({sys.executable})\n")
|
|
48
|
+
try:
|
|
49
|
+
config = load_config(args.project)
|
|
50
|
+
except ConfigError as exc:
|
|
51
|
+
out.write(f"project (none: {exc})\n")
|
|
52
|
+
return 0
|
|
53
|
+
dev = config.dev
|
|
54
|
+
argv = build_command(dev.command, host=dev.host, port=dev.port)
|
|
55
|
+
out.write(f"project {config.name}\n")
|
|
56
|
+
out.write(f"root {config.root}\n")
|
|
57
|
+
out.write(f"command {shlex.join(argv)}\n")
|
|
58
|
+
out.write(f"address http://{dev.host}:{dev.port}/\n")
|
|
59
|
+
out.write(f"watch {', '.join(dev.watch)}\n")
|
|
60
|
+
out.write(f"extensions {', '.join(dev.extensions) or '(all files)'}\n")
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _cmd_dev(args: argparse.Namespace, out: TextIO) -> int:
|
|
65
|
+
config = load_config(args.project)
|
|
66
|
+
dev = config.dev
|
|
67
|
+
host = dev.host if args.host is None else args.host
|
|
68
|
+
port = dev.port if args.port is None else args.port
|
|
69
|
+
interval = dev.interval if args.interval is None else args.interval
|
|
70
|
+
if not 0 < port < 65536:
|
|
71
|
+
raise CLIError(f"port must be between 1 and 65535 (got {port})")
|
|
72
|
+
if interval <= 0:
|
|
73
|
+
raise CLIError("interval must be positive")
|
|
74
|
+
argv = build_command(dev.command, host=host, port=port)
|
|
75
|
+
|
|
76
|
+
env = dict(os.environ)
|
|
77
|
+
env.update(AVALON_HOST=host, AVALON_PORT=str(port), AVALON_ENV="development")
|
|
78
|
+
server = DevServer(
|
|
79
|
+
argv,
|
|
80
|
+
cwd=config.root,
|
|
81
|
+
env=env,
|
|
82
|
+
watch=dev.watch,
|
|
83
|
+
extensions=dev.extensions,
|
|
84
|
+
ignore=dev.ignore,
|
|
85
|
+
interval=interval,
|
|
86
|
+
reload=not args.no_reload,
|
|
87
|
+
)
|
|
88
|
+
out.write(f"Serving {config.name} at http://{host}:{port}/ (Ctrl-C to stop)\n")
|
|
89
|
+
out.flush()
|
|
90
|
+
|
|
91
|
+
# Treat SIGTERM like a clean shutdown so the child is never orphaned.
|
|
92
|
+
stop_event = threading.Event()
|
|
93
|
+
previous = None
|
|
94
|
+
if threading.current_thread() is threading.main_thread():
|
|
95
|
+
previous = signal.signal(signal.SIGTERM, lambda signum, frame: stop_event.set())
|
|
96
|
+
try:
|
|
97
|
+
return server.run(stop_event)
|
|
98
|
+
except KeyboardInterrupt:
|
|
99
|
+
out.write("\nStopped.\n")
|
|
100
|
+
return 130
|
|
101
|
+
finally:
|
|
102
|
+
if previous is not None:
|
|
103
|
+
signal.signal(signal.SIGTERM, previous)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
107
|
+
"""Construct the argument parser for the ``avalon`` command."""
|
|
108
|
+
parser = argparse.ArgumentParser(
|
|
109
|
+
prog="avalon",
|
|
110
|
+
description="Command-line companion for the Avalon real-time web framework.",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
113
|
+
sub = parser.add_subparsers(dest="command", metavar="COMMAND", required=True)
|
|
114
|
+
|
|
115
|
+
new = sub.add_parser("new", help="create a new project from a template")
|
|
116
|
+
new.add_argument("name", help="project name (also the directory name)")
|
|
117
|
+
new.add_argument(
|
|
118
|
+
"-t", "--template", default=DEFAULT_TEMPLATE, choices=sorted(TEMPLATES),
|
|
119
|
+
help=f"template to use (default: {DEFAULT_TEMPLATE})",
|
|
120
|
+
)
|
|
121
|
+
new.add_argument(
|
|
122
|
+
"-d", "--directory", default=".",
|
|
123
|
+
help="parent directory to create the project in (default: current directory)",
|
|
124
|
+
)
|
|
125
|
+
new.add_argument("--force", action="store_true", help="write into a non-empty directory")
|
|
126
|
+
new.set_defaults(handler=_cmd_new)
|
|
127
|
+
|
|
128
|
+
dev = sub.add_parser("dev", help="run the dev command with auto-reload")
|
|
129
|
+
dev.add_argument("-C", "--project", default=".", help="project directory (default: current directory)")
|
|
130
|
+
dev.add_argument("--host", help="host to bind (overrides avalon.toml)")
|
|
131
|
+
dev.add_argument("-p", "--port", type=int, help="port to bind (overrides avalon.toml)")
|
|
132
|
+
dev.add_argument("--interval", type=float, help="seconds between file-change polls")
|
|
133
|
+
dev.add_argument("--no-reload", action="store_true", help="run once without watching files")
|
|
134
|
+
dev.set_defaults(handler=_cmd_dev)
|
|
135
|
+
|
|
136
|
+
templates = sub.add_parser("templates", help="list available project templates")
|
|
137
|
+
templates.set_defaults(handler=_cmd_templates)
|
|
138
|
+
|
|
139
|
+
info = sub.add_parser("info", help="show environment and resolved project settings")
|
|
140
|
+
info.add_argument("-C", "--project", default=".", help="project directory (default: current directory)")
|
|
141
|
+
info.set_defaults(handler=_cmd_info)
|
|
142
|
+
|
|
143
|
+
return parser
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main(argv: Sequence[str] | None = None, out: TextIO | None = None) -> int:
|
|
147
|
+
"""Entry point for the ``avalon`` console script.
|
|
148
|
+
|
|
149
|
+
:param argv: arguments excluding the program name (defaults to ``sys.argv[1:]``).
|
|
150
|
+
:param out: stream for normal output (defaults to ``sys.stdout``).
|
|
151
|
+
:returns: process exit code. Errors print ``error: ...`` to stderr and return 1.
|
|
152
|
+
"""
|
|
153
|
+
out = out or sys.stdout
|
|
154
|
+
args = build_parser().parse_args(argv)
|
|
155
|
+
try:
|
|
156
|
+
return args.handler(args, out)
|
|
157
|
+
except (CLIError, ConfigError, ScaffoldError, ValueError) as exc:
|
|
158
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
159
|
+
return 1
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
if __name__ == "__main__": # pragma: no cover
|
|
163
|
+
sys.exit(main())
|