sillo-start 0.1.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.
- sillo_start-0.1.0/.gitignore +36 -0
- sillo_start-0.1.0/PKG-INFO +153 -0
- sillo_start-0.1.0/README.md +126 -0
- sillo_start-0.1.0/pyproject.toml +76 -0
- sillo_start-0.1.0/sillo_start/__init__.py +35 -0
- sillo_start-0.1.0/sillo_start/__main__.py +19 -0
- sillo_start-0.1.0/sillo_start/cli/__init__.py +20 -0
- sillo_start-0.1.0/sillo_start/cli/app.py +106 -0
- sillo_start-0.1.0/sillo_start/cli/create.py +114 -0
- sillo_start-0.1.0/sillo_start/exceptions.py +69 -0
- sillo_start-0.1.0/sillo_start/project/__init__.py +10 -0
- sillo_start-0.1.0/sillo_start/project/template.py +225 -0
- sillo_start-0.1.0/sillo_start/utils/__init__.py +21 -0
- sillo_start-0.1.0/sillo_start/utils/console.py +220 -0
- sillo_start-0.1.0/sillo_start/utils/naming.py +148 -0
- sillo_start-0.1.0/sillo_start/utils/pkgmanagers.py +283 -0
- sillo_start-0.1.0/sillo_start/utils/subprocess.py +179 -0
- sillo_start-0.1.0/tests/conftest.py +80 -0
- sillo_start-0.1.0/tests/test_cli.py +205 -0
- sillo_start-0.1.0/tests/test_template.py +212 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# Tooling caches
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
.coverage.*
|
|
23
|
+
htmlcov/
|
|
24
|
+
coverage.xml
|
|
25
|
+
|
|
26
|
+
# Editors
|
|
27
|
+
.idea/
|
|
28
|
+
.vscode/
|
|
29
|
+
*.swp
|
|
30
|
+
|
|
31
|
+
# OS
|
|
32
|
+
.DS_Store
|
|
33
|
+
Thumbs.db
|
|
34
|
+
|
|
35
|
+
# uv.lock is committed deliberately: sillo-start is an application-style CLI,
|
|
36
|
+
# so a reproducible resolution is worth more than floating dependencies.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sillo-start
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Create a Sillo application from a starter repository.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sillohq/start
|
|
6
|
+
Project-URL: Repository, https://github.com/sillohq/start
|
|
7
|
+
Project-URL: Documentation, https://docs.sillo.build
|
|
8
|
+
Author-email: Chidebele Dunamis <techwithdunamix@gmail.com>
|
|
9
|
+
License-Expression: BSD-3-Clause
|
|
10
|
+
Keywords: asgi,cli,scaffolding,sillo,starter
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Requires-Dist: rich>=13.7.0
|
|
20
|
+
Requires-Dist: typer>=0.12.0
|
|
21
|
+
Provides-Extra: dev
|
|
22
|
+
Requires-Dist: mypy>=1.15.0; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest>=8.3.5; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Sillo Start
|
|
29
|
+
|
|
30
|
+
Creates a Sillo application from a starter repository.
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uvx sillo-start create-app myapp
|
|
34
|
+
cd myapp
|
|
35
|
+
make setup
|
|
36
|
+
make dev
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That is the whole tool. It fetches a real, working application, renames it to
|
|
40
|
+
yours, gives it its own secrets, and gets out of the way.
|
|
41
|
+
|
|
42
|
+
## Why a starter repository, not a generator
|
|
43
|
+
|
|
44
|
+
A generator renders templates. Templates are checked for *rendering*, which is
|
|
45
|
+
not the same as working — a project can produce valid Python, import cleanly,
|
|
46
|
+
render every page, and still fail on its first real request. Middleware
|
|
47
|
+
registered in the wrong order, an auth backend reading the wrong claim, a
|
|
48
|
+
missing static mount: all of them render perfectly.
|
|
49
|
+
|
|
50
|
+
[`sillohq/starter`](https://github.com/sillohq/starter) is a real application
|
|
51
|
+
with its own CI. Every push boots it and exercises every route, on three
|
|
52
|
+
Python versions. What you get has been run, not just written.
|
|
53
|
+
|
|
54
|
+
It also means the starter can be read, forked and improved on its own, and
|
|
55
|
+
that `sillo-start` never has to be released to fix a bug in what it produces.
|
|
56
|
+
|
|
57
|
+
## Install
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
# Run it without installing
|
|
61
|
+
uvx sillo-start create-app myapp
|
|
62
|
+
|
|
63
|
+
# Or install it
|
|
64
|
+
uv tool install sillo-start
|
|
65
|
+
pipx install sillo-start
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Python 3.11 or newer. The only dependencies are `typer` and `rich`.
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
sillo-start create-app myapp # the default starter
|
|
74
|
+
sillo-start create-app sillohq/starter myapp # named explicitly
|
|
75
|
+
sillo-start create-app sillohq/starter@v1.2 myapp # pinned to a tag
|
|
76
|
+
sillo-start create-app acme/our-template myapp # your own starter
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Any public GitHub repository works. A full URL is accepted too, so pasting what
|
|
80
|
+
is in your address bar does what you expect.
|
|
81
|
+
|
|
82
|
+
| Option | |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `--ref <branch\|tag>` | Which revision to take. Defaults to `main` |
|
|
85
|
+
| `-d`, `--directory <path>` | Where to create it. Defaults to `./<name>` |
|
|
86
|
+
| `--install` | Install dependencies straight away |
|
|
87
|
+
| `--no-git` | Do not initialise a git repository |
|
|
88
|
+
| `--force` | Allow a directory that is not empty |
|
|
89
|
+
| `-v`, `--verbose` | Show tracebacks |
|
|
90
|
+
|
|
91
|
+
Dependencies are **not** installed by default, so creating a project takes a
|
|
92
|
+
second rather than a minute. `make setup` in the new project does it, along
|
|
93
|
+
with everything else a first run needs.
|
|
94
|
+
|
|
95
|
+
## What it does to the project
|
|
96
|
+
|
|
97
|
+
The starter arrives as a tarball rather than a clone. That needs no `git` on
|
|
98
|
+
the machine, brings no history for you to delete before your first commit, and
|
|
99
|
+
pins to a tag as easily as to a branch.
|
|
100
|
+
|
|
101
|
+
Then three things happen:
|
|
102
|
+
|
|
103
|
+
**It takes your name.** The package name, the application title, the SQLite
|
|
104
|
+
path. Rewriting is targeted rather than a blanket find-and-replace, so prose
|
|
105
|
+
that happens to say "starter" — a README sentence, a comment — is left as
|
|
106
|
+
written.
|
|
107
|
+
|
|
108
|
+
**It gets its own secrets.** `.env` is created from `.env.example` with a fresh
|
|
109
|
+
key for `SECRET_KEY`, `JWT_SECRET` and `APP_KEY`. A secret committed to a
|
|
110
|
+
starter is a placeholder by definition; no two projects should ever share a
|
|
111
|
+
signing key. An existing `.env` is never touched — it may hold real credentials.
|
|
112
|
+
|
|
113
|
+
**It becomes a git repository**, unless you pass `--no-git`.
|
|
114
|
+
|
|
115
|
+
## What it deliberately does not do
|
|
116
|
+
|
|
117
|
+
Migrations, creating users, running the queue worker, starting the server —
|
|
118
|
+
none of that is here.
|
|
119
|
+
|
|
120
|
+
Those belong to the project, in its own `console.py`:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
python console.py db migrate
|
|
124
|
+
python console.py user admin ada@example.com ada
|
|
125
|
+
python console.py worker
|
|
126
|
+
python console.py serve --reload
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The framework provides the operations as plain functions —
|
|
130
|
+
`sillo.record.commands`, `sillo.users.commands`, `sillo.work.commands` — and
|
|
131
|
+
the project decides how to expose them. Nothing in a created project depends on
|
|
132
|
+
`sillo-start`, so it cannot rot when this tool changes, and you can delete this
|
|
133
|
+
tool the moment your project exists.
|
|
134
|
+
|
|
135
|
+
## Safety
|
|
136
|
+
|
|
137
|
+
An archive member whose path escapes the destination is refused rather than
|
|
138
|
+
sanitised — that is how a malicious archive overwrites files elsewhere on your
|
|
139
|
+
machine.
|
|
140
|
+
|
|
141
|
+
A directory that is not empty is refused unless you pass `--force`, and even
|
|
142
|
+
then existing files are left in place.
|
|
143
|
+
|
|
144
|
+
A project name that is not a usable Python identifier is refused *before*
|
|
145
|
+
anything is fetched, since it becomes both a package name and a directory.
|
|
146
|
+
|
|
147
|
+
## Development
|
|
148
|
+
|
|
149
|
+
See [docs/contributing.md](docs/contributing.md).
|
|
150
|
+
|
|
151
|
+
## Licence
|
|
152
|
+
|
|
153
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Sillo Start
|
|
2
|
+
|
|
3
|
+
Creates a Sillo application from a starter repository.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
uvx sillo-start create-app myapp
|
|
7
|
+
cd myapp
|
|
8
|
+
make setup
|
|
9
|
+
make dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
That is the whole tool. It fetches a real, working application, renames it to
|
|
13
|
+
yours, gives it its own secrets, and gets out of the way.
|
|
14
|
+
|
|
15
|
+
## Why a starter repository, not a generator
|
|
16
|
+
|
|
17
|
+
A generator renders templates. Templates are checked for *rendering*, which is
|
|
18
|
+
not the same as working — a project can produce valid Python, import cleanly,
|
|
19
|
+
render every page, and still fail on its first real request. Middleware
|
|
20
|
+
registered in the wrong order, an auth backend reading the wrong claim, a
|
|
21
|
+
missing static mount: all of them render perfectly.
|
|
22
|
+
|
|
23
|
+
[`sillohq/starter`](https://github.com/sillohq/starter) is a real application
|
|
24
|
+
with its own CI. Every push boots it and exercises every route, on three
|
|
25
|
+
Python versions. What you get has been run, not just written.
|
|
26
|
+
|
|
27
|
+
It also means the starter can be read, forked and improved on its own, and
|
|
28
|
+
that `sillo-start` never has to be released to fix a bug in what it produces.
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Run it without installing
|
|
34
|
+
uvx sillo-start create-app myapp
|
|
35
|
+
|
|
36
|
+
# Or install it
|
|
37
|
+
uv tool install sillo-start
|
|
38
|
+
pipx install sillo-start
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Python 3.11 or newer. The only dependencies are `typer` and `rich`.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
sillo-start create-app myapp # the default starter
|
|
47
|
+
sillo-start create-app sillohq/starter myapp # named explicitly
|
|
48
|
+
sillo-start create-app sillohq/starter@v1.2 myapp # pinned to a tag
|
|
49
|
+
sillo-start create-app acme/our-template myapp # your own starter
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Any public GitHub repository works. A full URL is accepted too, so pasting what
|
|
53
|
+
is in your address bar does what you expect.
|
|
54
|
+
|
|
55
|
+
| Option | |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `--ref <branch\|tag>` | Which revision to take. Defaults to `main` |
|
|
58
|
+
| `-d`, `--directory <path>` | Where to create it. Defaults to `./<name>` |
|
|
59
|
+
| `--install` | Install dependencies straight away |
|
|
60
|
+
| `--no-git` | Do not initialise a git repository |
|
|
61
|
+
| `--force` | Allow a directory that is not empty |
|
|
62
|
+
| `-v`, `--verbose` | Show tracebacks |
|
|
63
|
+
|
|
64
|
+
Dependencies are **not** installed by default, so creating a project takes a
|
|
65
|
+
second rather than a minute. `make setup` in the new project does it, along
|
|
66
|
+
with everything else a first run needs.
|
|
67
|
+
|
|
68
|
+
## What it does to the project
|
|
69
|
+
|
|
70
|
+
The starter arrives as a tarball rather than a clone. That needs no `git` on
|
|
71
|
+
the machine, brings no history for you to delete before your first commit, and
|
|
72
|
+
pins to a tag as easily as to a branch.
|
|
73
|
+
|
|
74
|
+
Then three things happen:
|
|
75
|
+
|
|
76
|
+
**It takes your name.** The package name, the application title, the SQLite
|
|
77
|
+
path. Rewriting is targeted rather than a blanket find-and-replace, so prose
|
|
78
|
+
that happens to say "starter" — a README sentence, a comment — is left as
|
|
79
|
+
written.
|
|
80
|
+
|
|
81
|
+
**It gets its own secrets.** `.env` is created from `.env.example` with a fresh
|
|
82
|
+
key for `SECRET_KEY`, `JWT_SECRET` and `APP_KEY`. A secret committed to a
|
|
83
|
+
starter is a placeholder by definition; no two projects should ever share a
|
|
84
|
+
signing key. An existing `.env` is never touched — it may hold real credentials.
|
|
85
|
+
|
|
86
|
+
**It becomes a git repository**, unless you pass `--no-git`.
|
|
87
|
+
|
|
88
|
+
## What it deliberately does not do
|
|
89
|
+
|
|
90
|
+
Migrations, creating users, running the queue worker, starting the server —
|
|
91
|
+
none of that is here.
|
|
92
|
+
|
|
93
|
+
Those belong to the project, in its own `console.py`:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
python console.py db migrate
|
|
97
|
+
python console.py user admin ada@example.com ada
|
|
98
|
+
python console.py worker
|
|
99
|
+
python console.py serve --reload
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The framework provides the operations as plain functions —
|
|
103
|
+
`sillo.record.commands`, `sillo.users.commands`, `sillo.work.commands` — and
|
|
104
|
+
the project decides how to expose them. Nothing in a created project depends on
|
|
105
|
+
`sillo-start`, so it cannot rot when this tool changes, and you can delete this
|
|
106
|
+
tool the moment your project exists.
|
|
107
|
+
|
|
108
|
+
## Safety
|
|
109
|
+
|
|
110
|
+
An archive member whose path escapes the destination is refused rather than
|
|
111
|
+
sanitised — that is how a malicious archive overwrites files elsewhere on your
|
|
112
|
+
machine.
|
|
113
|
+
|
|
114
|
+
A directory that is not empty is refused unless you pass `--force`, and even
|
|
115
|
+
then existing files are left in place.
|
|
116
|
+
|
|
117
|
+
A project name that is not a usable Python identifier is refused *before*
|
|
118
|
+
anything is fetched, since it becomes both a package name and a directory.
|
|
119
|
+
|
|
120
|
+
## Development
|
|
121
|
+
|
|
122
|
+
See [docs/contributing.md](docs/contributing.md).
|
|
123
|
+
|
|
124
|
+
## Licence
|
|
125
|
+
|
|
126
|
+
BSD-3-Clause.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sillo-start"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Create a Sillo application from a starter repository."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "BSD-3-Clause"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Chidebele Dunamis", email = "techwithdunamix@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["sillo", "scaffolding", "cli", "starter", "asgi"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Code Generators",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"typer>=0.12.0",
|
|
27
|
+
"rich>=13.7.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=8.3.5",
|
|
33
|
+
"pytest-cov>=5.0.0",
|
|
34
|
+
"mypy>=1.15.0",
|
|
35
|
+
"ruff>=0.6.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/sillohq/start"
|
|
40
|
+
Repository = "https://github.com/sillohq/start"
|
|
41
|
+
Documentation = "https://docs.sillo.build"
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
sillo-start = "sillo_start.__main__:main"
|
|
45
|
+
|
|
46
|
+
[tool.hatch.build.targets.wheel]
|
|
47
|
+
packages = ["sillo_start"]
|
|
48
|
+
|
|
49
|
+
[tool.hatch.build.targets.sdist]
|
|
50
|
+
include = ["sillo_start", "tests", "README.md"]
|
|
51
|
+
|
|
52
|
+
[tool.pytest.ini_options]
|
|
53
|
+
testpaths = ["tests"]
|
|
54
|
+
addopts = "-q"
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 88
|
|
58
|
+
target-version = "py311"
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
62
|
+
ignore = [
|
|
63
|
+
"E501",
|
|
64
|
+
# Typer expresses defaults as function calls in the signature.
|
|
65
|
+
"B008",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint.per-file-ignores]
|
|
69
|
+
"__init__.py" = ["F401"]
|
|
70
|
+
|
|
71
|
+
[tool.mypy]
|
|
72
|
+
python_version = "3.11"
|
|
73
|
+
ignore_missing_imports = true
|
|
74
|
+
warn_unused_ignores = true
|
|
75
|
+
disallow_untyped_defs = false
|
|
76
|
+
files = ["sillo_start"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Sillo Start — bootstrapper and orchestrator for Sillo applications.
|
|
2
|
+
|
|
3
|
+
Sillo Start creates properly structured Sillo projects, installs and removes
|
|
4
|
+
features against an existing project, and runs the development services a
|
|
5
|
+
project needs. ``sillo.toml`` at the project root is the authoritative record
|
|
6
|
+
of what a project is; every command reads and updates it.
|
|
7
|
+
|
|
8
|
+
The package is layered so the CLI stays a thin shell over reusable logic:
|
|
9
|
+
|
|
10
|
+
``config``
|
|
11
|
+
The manifest schema and its loading/writing.
|
|
12
|
+
``operations``
|
|
13
|
+
Reversible units of work (write a file, edit TOML, install a package) and
|
|
14
|
+
the transaction that applies them or rolls them back.
|
|
15
|
+
``packages``
|
|
16
|
+
The package-group registry, dependency resolver, and installers.
|
|
17
|
+
``blueprints``
|
|
18
|
+
Named project archetypes that turn wizard answers into a manifest.
|
|
19
|
+
``project``
|
|
20
|
+
Creation, inspection and validation of projects on disk.
|
|
21
|
+
``generators``
|
|
22
|
+
Component scaffolding for an existing project.
|
|
23
|
+
``orchestration``
|
|
24
|
+
The supervised process manager behind ``sillo-start dev``.
|
|
25
|
+
``prompts``
|
|
26
|
+
The interactive wizard.
|
|
27
|
+
``cli``
|
|
28
|
+
Typer commands, which do argument handling and rendering only.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
__version__ = "0.1.0"
|
|
34
|
+
|
|
35
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""``python -m sillo_start`` and the ``sillo-start`` console script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
"""Load the command tree and run the CLI."""
|
|
8
|
+
# Imported here rather than at module scope so that plugin loading and
|
|
9
|
+
# command registration happen once, at invocation, and a broken plugin
|
|
10
|
+
# cannot make `python -m sillo_start` unimportable.
|
|
11
|
+
from .cli import build_cli
|
|
12
|
+
|
|
13
|
+
# A Typer app is called, not `.main()`ed — calling it is what runs the
|
|
14
|
+
# underlying Click command with argv.
|
|
15
|
+
build_cli()()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
if __name__ == "__main__":
|
|
19
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""The Typer command tree.
|
|
2
|
+
|
|
3
|
+
Importing the command module registers its command on the shared ``app``, so
|
|
4
|
+
:func:`build_cli` imports it and returns the app.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_cli() -> typer.Typer:
|
|
13
|
+
"""Assemble and return the CLI application."""
|
|
14
|
+
from . import create # noqa: F401 — importing registers the command
|
|
15
|
+
from .app import app
|
|
16
|
+
|
|
17
|
+
return app
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
__all__ = ["build_cli"]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""The Typer application and its shared behaviour.
|
|
2
|
+
|
|
3
|
+
The CLI layer stays thin: it parses arguments, calls into
|
|
4
|
+
:mod:`sillo_start.project.template`, and renders the result. The fetching and
|
|
5
|
+
personalising happen there, which is what lets them be driven from tests
|
|
6
|
+
without a terminal.
|
|
7
|
+
|
|
8
|
+
Errors are handled in one place. Anything deriving from
|
|
9
|
+
:class:`~sillo_start.exceptions.SilloStartError` becomes a clean message plus
|
|
10
|
+
its hint and a predictable exit code; the traceback appears only under
|
|
11
|
+
``--verbose``.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
from collections.abc import Callable
|
|
18
|
+
from functools import wraps
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import typer
|
|
22
|
+
|
|
23
|
+
from .. import __version__
|
|
24
|
+
from ..exceptions import SilloStartError
|
|
25
|
+
from ..utils.console import console
|
|
26
|
+
|
|
27
|
+
app = typer.Typer(
|
|
28
|
+
name="sillo-start",
|
|
29
|
+
help="Create a Sillo application from a starter repository.",
|
|
30
|
+
add_completion=True,
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
rich_markup_mode="rich",
|
|
33
|
+
context_settings={"help_option_names": ["-h", "--help"]},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def version_callback(value: bool) -> None:
|
|
38
|
+
"""Print the version and exit."""
|
|
39
|
+
if value:
|
|
40
|
+
console.print(f"sillo-start {__version__}")
|
|
41
|
+
raise typer.Exit()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.callback()
|
|
45
|
+
def main(
|
|
46
|
+
version: bool = typer.Option(
|
|
47
|
+
False,
|
|
48
|
+
"--version",
|
|
49
|
+
"-V",
|
|
50
|
+
help="Show the version and exit.",
|
|
51
|
+
callback=version_callback,
|
|
52
|
+
is_eager=True,
|
|
53
|
+
),
|
|
54
|
+
verbose: bool = typer.Option(
|
|
55
|
+
False, "--verbose", "-v", help="Show debug output and tracebacks."
|
|
56
|
+
),
|
|
57
|
+
quiet: bool = typer.Option(
|
|
58
|
+
False, "--quiet", "-q", help="Suppress non-essential output."
|
|
59
|
+
),
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Sillo Start — create a Sillo application from a starter repository."""
|
|
62
|
+
console.configure(verbose=verbose, quiet=quiet)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def handle_errors(func: Callable[..., Any]) -> Callable[..., Any]:
|
|
66
|
+
"""Turn deliberate failures into clean messages and exit codes.
|
|
67
|
+
|
|
68
|
+
Applied to every command body. ``typer.Exit`` and ``typer.Abort`` pass
|
|
69
|
+
through untouched so control flow still works.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@wraps(func)
|
|
73
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
74
|
+
try:
|
|
75
|
+
return func(*args, **kwargs)
|
|
76
|
+
except (typer.Exit, typer.Abort):
|
|
77
|
+
raise
|
|
78
|
+
except SilloStartError as exc:
|
|
79
|
+
console.error(exc.message, hint=exc.hint)
|
|
80
|
+
if console.verbose:
|
|
81
|
+
console.rich.print_exception()
|
|
82
|
+
raise typer.Exit(code=exc.exit_code) from exc
|
|
83
|
+
except KeyboardInterrupt:
|
|
84
|
+
console.blank()
|
|
85
|
+
console.warning("Cancelled.")
|
|
86
|
+
raise typer.Exit(code=130) from None
|
|
87
|
+
except Exception as exc: # noqa: BLE001 — last resort, reported not swallowed
|
|
88
|
+
console.error(
|
|
89
|
+
f"Unexpected error: {exc}",
|
|
90
|
+
hint="Re-run with --verbose for the full traceback, and please report this.",
|
|
91
|
+
)
|
|
92
|
+
if console.verbose:
|
|
93
|
+
console.rich.print_exception()
|
|
94
|
+
raise typer.Exit(code=1) from exc
|
|
95
|
+
|
|
96
|
+
return wrapper
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run() -> None:
|
|
100
|
+
"""Entrypoint used by the console script."""
|
|
101
|
+
try:
|
|
102
|
+
app()
|
|
103
|
+
except SilloStartError as exc:
|
|
104
|
+
# Reached only for failures raised outside a command body.
|
|
105
|
+
console.error(exc.message, hint=exc.hint)
|
|
106
|
+
sys.exit(exc.exit_code)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""``sillo-start create-app``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from ..exceptions import UsageError
|
|
10
|
+
from ..utils.console import console
|
|
11
|
+
from ..utils.naming import is_valid_project_name
|
|
12
|
+
from .app import app, handle_errors
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command("create-app")
|
|
16
|
+
@handle_errors
|
|
17
|
+
def create_app(
|
|
18
|
+
template: str = typer.Argument(
|
|
19
|
+
None, help="Starter repository, e.g. sillohq/starter or sillohq/starter@v1."
|
|
20
|
+
),
|
|
21
|
+
name: str = typer.Argument(None, help="Project name. Also the directory name."),
|
|
22
|
+
directory: Path = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--directory",
|
|
25
|
+
"-d",
|
|
26
|
+
help="Where to create the project. Defaults to ./<name>.",
|
|
27
|
+
),
|
|
28
|
+
ref: str = typer.Option(
|
|
29
|
+
None, "--ref", help="Branch or tag to take. Defaults to main."
|
|
30
|
+
),
|
|
31
|
+
install: bool = typer.Option(
|
|
32
|
+
False, "--install/--no-install", help="Install dependencies after fetching."
|
|
33
|
+
),
|
|
34
|
+
git: bool = typer.Option(
|
|
35
|
+
True, "--git/--no-git", help="Initialise a git repository."
|
|
36
|
+
),
|
|
37
|
+
force: bool = typer.Option(False, "--force", help="Allow a non-empty directory."),
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Create a project from a starter repository.
|
|
40
|
+
|
|
41
|
+
sillo-start create-app myapp
|
|
42
|
+
sillo-start create-app sillohq/starter myapp
|
|
43
|
+
sillo-start create-app sillohq/starter@v1.2 myapp
|
|
44
|
+
|
|
45
|
+
The starter is a real application with its own CI, so what you get has been
|
|
46
|
+
booted and exercised rather than only rendered. With one argument the
|
|
47
|
+
default starter is used and the argument is the project name.
|
|
48
|
+
"""
|
|
49
|
+
from ..project.template import DEFAULT_TEMPLATE, Template, fetch, personalise
|
|
50
|
+
|
|
51
|
+
# One argument is the project name; the starter is only ever given when
|
|
52
|
+
# both are, so `create-app myapp` does the obvious thing.
|
|
53
|
+
if name is None:
|
|
54
|
+
name, template = template, DEFAULT_TEMPLATE
|
|
55
|
+
if not name:
|
|
56
|
+
raise UsageError(
|
|
57
|
+
"A project name is required.",
|
|
58
|
+
hint="sillo-start create-app myapp",
|
|
59
|
+
)
|
|
60
|
+
if not is_valid_project_name(name):
|
|
61
|
+
raise UsageError(
|
|
62
|
+
f"'{name}' is not a valid project name.",
|
|
63
|
+
hint="Use a letter followed by letters, digits, hyphens or underscores.",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
parsed = Template.parse(template or DEFAULT_TEMPLATE, ref=ref)
|
|
67
|
+
root = (directory or Path.cwd() / name).resolve()
|
|
68
|
+
if root.exists() and any(root.iterdir()) and not force:
|
|
69
|
+
raise UsageError(
|
|
70
|
+
f"{root} is not empty.",
|
|
71
|
+
hint="Choose another directory, or pass --force.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
console.header(f"Creating {name}", f"from {parsed.slug}@{parsed.ref}")
|
|
75
|
+
with console.progress(f"Fetching {parsed.slug}…"):
|
|
76
|
+
fetch(parsed, root)
|
|
77
|
+
|
|
78
|
+
changed = personalise(root, name)
|
|
79
|
+
console.success(f"Fetched {parsed.slug} and renamed {len(changed)} file(s)")
|
|
80
|
+
|
|
81
|
+
if git:
|
|
82
|
+
from ..utils.subprocess import run, tool_exists
|
|
83
|
+
|
|
84
|
+
if tool_exists("git") and not (root / ".git").exists():
|
|
85
|
+
run(["git", "init", "--quiet"], cwd=root, check=False)
|
|
86
|
+
|
|
87
|
+
if install:
|
|
88
|
+
from ..utils.pkgmanagers import detect_python_manager
|
|
89
|
+
from ..utils.subprocess import run
|
|
90
|
+
|
|
91
|
+
manager = detect_python_manager()
|
|
92
|
+
console.header("Dependencies", f"installing with {manager.name}")
|
|
93
|
+
with console.progress("Resolving…"):
|
|
94
|
+
result = run(manager.sync_command(), cwd=root, check=False, timeout=900)
|
|
95
|
+
if not result.ok:
|
|
96
|
+
console.failure(f"{manager.name} exited with code {result.returncode}.")
|
|
97
|
+
if result.output:
|
|
98
|
+
console.raw(result.output)
|
|
99
|
+
raise typer.Exit(code=1)
|
|
100
|
+
console.success("Dependencies installed.")
|
|
101
|
+
|
|
102
|
+
console.blank()
|
|
103
|
+
console.print("[bold]Next steps[/bold]")
|
|
104
|
+
steps = [f"cd {root.name}"]
|
|
105
|
+
if not install:
|
|
106
|
+
steps.append("make setup")
|
|
107
|
+
else:
|
|
108
|
+
steps.append("make migrate")
|
|
109
|
+
steps.append("make dev")
|
|
110
|
+
console.commands(steps)
|
|
111
|
+
console.blank()
|
|
112
|
+
console.hint(
|
|
113
|
+
"The starter's README covers configuration, migrations and deployment."
|
|
114
|
+
)
|