tses 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.
- tses-0.1.0/.github/workflows/ci.yaml +26 -0
- tses-0.1.0/.github/workflows/publish.yaml +33 -0
- tses-0.1.0/.gitignore +12 -0
- tses-0.1.0/LICENSE +7 -0
- tses-0.1.0/PKG-INFO +79 -0
- tses-0.1.0/README.md +57 -0
- tses-0.1.0/pyproject.toml +52 -0
- tses-0.1.0/src/tses/__init__.py +3 -0
- tses-0.1.0/src/tses/__main__.py +7 -0
- tses-0.1.0/src/tses/cli.py +79 -0
- tses-0.1.0/src/tses/commands/__init__.py +1 -0
- tses-0.1.0/src/tses/commands/startproject.py +75 -0
- tses-0.1.0/src/tses/core/__init__.py +1 -0
- tses-0.1.0/src/tses/core/errors.py +17 -0
- tses-0.1.0/src/tses/core/filesystem.py +91 -0
- tses-0.1.0/src/tses/core/generator.py +90 -0
- tses-0.1.0/src/tses/core/headers.py +78 -0
- tses-0.1.0/src/tses/core/metadata.py +55 -0
- tses-0.1.0/src/tses/core/renderer.py +48 -0
- tses-0.1.0/src/tses/templates/__init__.py +1 -0
- tses-0.1.0/src/tses/templates/django/.docker/Dockerfile +22 -0
- tses-0.1.0/src/tses/templates/django/.docker/entrypoint.sh +15 -0
- tses-0.1.0/src/tses/templates/django/.dockerignore +24 -0
- tses-0.1.0/src/tses/templates/django/.env.example +32 -0
- tses-0.1.0/src/tses/templates/django/.github/workflows/deploy.yaml +42 -0
- tses-0.1.0/src/tses/templates/django/.gitignore +27 -0
- tses-0.1.0/src/tses/templates/django/README.md +442 -0
- tses-0.1.0/src/tses/templates/django/api/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/authentication/__init__.py +1 -0
- tses-0.1.0/src/tses/templates/django/api/authentication/utils/__init__.py +3 -0
- tses-0.1.0/src/tses/templates/django/api/authentication/utils/base_auth.py +35 -0
- tses-0.1.0/src/tses/templates/django/api/authorization/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/authorization/admin.py +3 -0
- tses-0.1.0/src/tses/templates/django/api/authorization/apps.py +6 -0
- tses-0.1.0/src/tses/templates/django/api/authorization/migrations/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/authorization/models/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/authorization/serializers/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/authorization/tests.py +3 -0
- tses-0.1.0/src/tses/templates/django/api/authorization/views/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/notification/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/notification/admin.py +3 -0
- tses-0.1.0/src/tses/templates/django/api/notification/apps.py +6 -0
- tses-0.1.0/src/tses/templates/django/api/notification/enums.py +26 -0
- tses-0.1.0/src/tses/templates/django/api/notification/migrations/0001_initial.py +34 -0
- tses-0.1.0/src/tses/templates/django/api/notification/migrations/0002_initial.py +31 -0
- tses-0.1.0/src/tses/templates/django/api/notification/migrations/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/notification/models.py +244 -0
- tses-0.1.0/src/tses/templates/django/api/notification/serializers.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/notification/tests.py +3 -0
- tses-0.1.0/src/tses/templates/django/api/notification/views.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/users/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/users/admin.py +3 -0
- tses-0.1.0/src/tses/templates/django/api/users/apps.py +6 -0
- tses-0.1.0/src/tses/templates/django/api/users/enums.py +31 -0
- tses-0.1.0/src/tses/templates/django/api/users/migrations/0001_initial.py +72 -0
- tses-0.1.0/src/tses/templates/django/api/users/migrations/0002_alter_user_created_by_alter_user_updated_by.py +25 -0
- tses-0.1.0/src/tses/templates/django/api/users/migrations/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/users/models/__init__.py +7 -0
- tses-0.1.0/src/tses/templates/django/api/users/models/activity_log.py +123 -0
- tses-0.1.0/src/tses/templates/django/api/users/models/manager.py +37 -0
- tses-0.1.0/src/tses/templates/django/api/users/models/user.py +37 -0
- tses-0.1.0/src/tses/templates/django/api/users/serializers/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/api/users/tests.py +3 -0
- tses-0.1.0/src/tses/templates/django/api/users/views/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/config/__init__.py +3 -0
- tses-0.1.0/src/tses/templates/django/config/asgi.py +16 -0
- tses-0.1.0/src/tses/templates/django/config/celery.py +19 -0
- tses-0.1.0/src/tses/templates/django/config/settings/__init__.py +25 -0
- tses-0.1.0/src/tses/templates/django/config/settings/app_registry.py +26 -0
- tses-0.1.0/src/tses/templates/django/config/settings/celery.py +12 -0
- tses-0.1.0/src/tses/templates/django/config/settings/commons.py +115 -0
- tses-0.1.0/src/tses/templates/django/config/settings/cors.py +12 -0
- tses-0.1.0/src/tses/templates/django/config/settings/db.py +12 -0
- tses-0.1.0/src/tses/templates/django/config/settings/drf.py +38 -0
- tses-0.1.0/src/tses/templates/django/config/settings/exception.py +6 -0
- tses-0.1.0/src/tses/templates/django/config/settings/jwt.py +31 -0
- tses-0.1.0/src/tses/templates/django/config/settings/middleware.py +11 -0
- tses-0.1.0/src/tses/templates/django/config/settings/s3.py +0 -0
- tses-0.1.0/src/tses/templates/django/config/settings/smtp.py +16 -0
- tses-0.1.0/src/tses/templates/django/config/urls.py +22 -0
- tses-0.1.0/src/tses/templates/django/config/wsgi.py +16 -0
- tses-0.1.0/src/tses/templates/django/docker-compose.yaml +52 -0
- tses-0.1.0/src/tses/templates/django/includes/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/includes/helpers/__init__.py +11 -0
- tses-0.1.0/src/tses/templates/django/includes/helpers/formatter.py +25 -0
- tses-0.1.0/src/tses/templates/django/includes/helpers/models.py +73 -0
- tses-0.1.0/src/tses/templates/django/includes/helpers/pagination.py +217 -0
- tses-0.1.0/src/tses/templates/django/includes/third_party/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/django/includes/third_party/exceptions.py +42 -0
- tses-0.1.0/src/tses/templates/django/includes/third_party/processor.py +59 -0
- tses-0.1.0/src/tses/templates/django/manage.py +22 -0
- tses-0.1.0/src/tses/templates/django/requirements.txt +11 -0
- tses-0.1.0/src/tses/templates/fastapi/.docker/Dockerfile +22 -0
- tses-0.1.0/src/tses/templates/fastapi/.docker/entrypoint.sh +8 -0
- tses-0.1.0/src/tses/templates/fastapi/.dockerignore +10 -0
- tses-0.1.0/src/tses/templates/fastapi/.env.example +29 -0
- tses-0.1.0/src/tses/templates/fastapi/.github/workflows/deploy.yaml +42 -0
- tses-0.1.0/src/tses/templates/fastapi/README.md +516 -0
- tses-0.1.0/src/tses/templates/fastapi/alembic/env.py +78 -0
- tses-0.1.0/src/tses/templates/fastapi/alembic/script.py.mako +20 -0
- tses-0.1.0/src/tses/templates/fastapi/alembic/versions/0001_initial.py +73 -0
- tses-0.1.0/src/tses/templates/fastapi/alembic/versions/__init__.py +1 -0
- tses-0.1.0/src/tses/templates/fastapi/alembic.ini +36 -0
- tses-0.1.0/src/tses/templates/fastapi/api/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/fastapi/api/auth/__init__.py +24 -0
- tses-0.1.0/src/tses/templates/fastapi/api/auth/base_auth.py +35 -0
- tses-0.1.0/src/tses/templates/fastapi/api/auth/schemas.py +15 -0
- tses-0.1.0/src/tses/templates/fastapi/api/auth/services/__init__.py +8 -0
- tses-0.1.0/src/tses/templates/fastapi/api/auth/services/auth.py +94 -0
- tses-0.1.0/src/tses/templates/fastapi/api/auth/services/token.py +86 -0
- tses-0.1.0/src/tses/templates/fastapi/api/health/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/fastapi/api/health/routers.py +44 -0
- tses-0.1.0/src/tses/templates/fastapi/api/health/schemas.py +24 -0
- tses-0.1.0/src/tses/templates/fastapi/api/health/service.py +59 -0
- tses-0.1.0/src/tses/templates/fastapi/api/users/__init__.py +1 -0
- tses-0.1.0/src/tses/templates/fastapi/api/users/models/__init__.py +3 -0
- tses-0.1.0/src/tses/templates/fastapi/api/users/models/users.py +67 -0
- tses-0.1.0/src/tses/templates/fastapi/api/users/routers.py +12 -0
- tses-0.1.0/src/tses/templates/fastapi/api/users/schemas.py +24 -0
- tses-0.1.0/src/tses/templates/fastapi/config/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/fastapi/config/db.py +99 -0
- tses-0.1.0/src/tses/templates/fastapi/config/settings.py +161 -0
- tses-0.1.0/src/tses/templates/fastapi/docker-compose.yaml +20 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/__init__.py +1 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/errors.py +57 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/exception_handlers.py +155 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/libs/__init__.py +4 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/libs/s3.py +348 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/middleware/__init__.py +3 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/middleware/error_handler.py +30 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/models.py +170 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/pagination.py +148 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/schemas.py +52 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/third_party/__init__.py +0 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/third_party/exceptions.py +42 -0
- tses-0.1.0/src/tses/templates/fastapi/includes/third_party/processor.py +59 -0
- tses-0.1.0/src/tses/templates/fastapi/main.py +72 -0
- tses-0.1.0/src/tses/templates/fastapi/requirements.txt +13 -0
- tses-0.1.0/tests/conftest.py +13 -0
- tses-0.1.0/tests/fixtures/.gitkeep +0 -0
- tses-0.1.0/tests/test_cli.py +41 -0
- tses-0.1.0/tests/test_force_mode.py +34 -0
- tses-0.1.0/tests/test_generation.py +51 -0
- tses-0.1.0/tests/test_headers.py +52 -0
- tses-0.1.0/tests/test_packaging.py +14 -0
- tses-0.1.0/tools/sync_scaffolds.py +108 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
|
|
11
|
+
steps:
|
|
12
|
+
- name: Checkout code
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Set up Python
|
|
16
|
+
uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
|
|
20
|
+
- name: Install dependencies
|
|
21
|
+
run: |
|
|
22
|
+
python -m pip install --upgrade pip
|
|
23
|
+
python -m pip install .[dev]
|
|
24
|
+
|
|
25
|
+
- name: Run tests
|
|
26
|
+
run: python -m pytest
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
steps:
|
|
14
|
+
- name: Checkout code
|
|
15
|
+
uses: actions/checkout@v4
|
|
16
|
+
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.12"
|
|
21
|
+
|
|
22
|
+
- name: Install build tooling
|
|
23
|
+
run: |
|
|
24
|
+
python -m pip install --upgrade pip
|
|
25
|
+
python -m pip install build
|
|
26
|
+
|
|
27
|
+
- name: Build package
|
|
28
|
+
run: python -m build
|
|
29
|
+
|
|
30
|
+
- name: Publish to PyPI
|
|
31
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
32
|
+
with:
|
|
33
|
+
password: ${{ secrets.PYPI_API_TOKEN }}
|
tses-0.1.0/.gitignore
ADDED
tses-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 TSES.
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
This repository and its contents are proprietary to TSES.
|
|
5
|
+
No permission is granted to use, copy, modify, distribute, sublicense,
|
|
6
|
+
or create derivative works from this software except by prior written
|
|
7
|
+
agreement with TSES.
|
tses-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tses
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TSES project scaffold generator for Django and FastAPI services.
|
|
5
|
+
Author: TSES
|
|
6
|
+
License: Proprietary
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: cli,django,fastapi,scaffold,tses
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: build>=1.2.2.post1; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest>=8.4.0; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# TSES
|
|
24
|
+
|
|
25
|
+
`tses` is the TSES scaffold generator CLI for bootstrapping curated Django and FastAPI service projects.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
For local development:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e .[dev]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
For end users after publishing:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install tses
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A `pipx install tses` workflow can also be used for isolated CLI installs.
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
Generate a Django scaffold into a new directory:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
tses startproject my-django-service -f django
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Generate a FastAPI scaffold into the current directory:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
tses startproject . -f fastapi --force
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Available options:
|
|
58
|
+
|
|
59
|
+
- `-f`, `--framework`: required framework choice, `django` or `fastapi`
|
|
60
|
+
- `--force`: overwrite scaffold-managed files in a non-empty target directory
|
|
61
|
+
- `--dry-run`: print planned file actions without writing files
|
|
62
|
+
- `--verbose`: print per-file actions during generation
|
|
63
|
+
- `--version`: print the installed `tses` version
|
|
64
|
+
|
|
65
|
+
## Development
|
|
66
|
+
|
|
67
|
+
Scaffold snapshots are vendored into `src/tses/templates`.
|
|
68
|
+
Use the sync script to refresh them from the scaffold repos:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
python tools/sync_scaffolds.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Default source repos:
|
|
75
|
+
|
|
76
|
+
- `/home/mike/django-scaffold`
|
|
77
|
+
- `/home/mike/fastapi-scaffold`
|
|
78
|
+
|
|
79
|
+
Before the first public release, confirm that the package name `tses` is available on the intended package index.
|
tses-0.1.0/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# TSES
|
|
2
|
+
|
|
3
|
+
`tses` is the TSES scaffold generator CLI for bootstrapping curated Django and FastAPI service projects.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
For local development:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e .[dev]
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For end users after publishing:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install tses
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A `pipx install tses` workflow can also be used for isolated CLI installs.
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
Generate a Django scaffold into a new directory:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
tses startproject my-django-service -f django
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Generate a FastAPI scaffold into the current directory:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
tses startproject . -f fastapi --force
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Available options:
|
|
36
|
+
|
|
37
|
+
- `-f`, `--framework`: required framework choice, `django` or `fastapi`
|
|
38
|
+
- `--force`: overwrite scaffold-managed files in a non-empty target directory
|
|
39
|
+
- `--dry-run`: print planned file actions without writing files
|
|
40
|
+
- `--verbose`: print per-file actions during generation
|
|
41
|
+
- `--version`: print the installed `tses` version
|
|
42
|
+
|
|
43
|
+
## Development
|
|
44
|
+
|
|
45
|
+
Scaffold snapshots are vendored into `src/tses/templates`.
|
|
46
|
+
Use the sync script to refresh them from the scaffold repos:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python tools/sync_scaffolds.py
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Default source repos:
|
|
53
|
+
|
|
54
|
+
- `/home/mike/django-scaffold`
|
|
55
|
+
- `/home/mike/fastapi-scaffold`
|
|
56
|
+
|
|
57
|
+
Before the first public release, confirm that the package name `tses` is available on the intended package index.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tses"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "TSES project scaffold generator for Django and FastAPI services."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Proprietary" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "TSES" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["tses", "scaffold", "django", "fastapi", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Software Development :: Code Generators",
|
|
24
|
+
"Topic :: Utilities",
|
|
25
|
+
]
|
|
26
|
+
dependencies = []
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
dev = [
|
|
30
|
+
"build>=1.2.2.post1",
|
|
31
|
+
"pytest>=8.4.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
tses = "tses.cli:run"
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel]
|
|
38
|
+
packages = ["src/tses"]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = [
|
|
42
|
+
"/src/tses",
|
|
43
|
+
"/README.md",
|
|
44
|
+
"/LICENSE",
|
|
45
|
+
"/tools",
|
|
46
|
+
"/tests",
|
|
47
|
+
"/.github/workflows",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
addopts = "-q"
|
|
52
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Command-line interface for the TSES scaffold generator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from tses import __version__
|
|
9
|
+
from tses.commands.startproject import handle_startproject
|
|
10
|
+
from tses.core.errors import TsesError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
"""Build the top-level CLI parser."""
|
|
15
|
+
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="tses",
|
|
18
|
+
description="Generate curated Django or FastAPI project scaffolds.",
|
|
19
|
+
)
|
|
20
|
+
parser.add_argument(
|
|
21
|
+
"--version",
|
|
22
|
+
action="version",
|
|
23
|
+
version=f"tses {__version__}",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
27
|
+
|
|
28
|
+
startproject_parser = subparsers.add_parser(
|
|
29
|
+
"startproject",
|
|
30
|
+
help="Generate a project scaffold into the target directory.",
|
|
31
|
+
)
|
|
32
|
+
startproject_parser.add_argument(
|
|
33
|
+
"target",
|
|
34
|
+
help="Target directory to generate into. Use '.' for the current directory.",
|
|
35
|
+
)
|
|
36
|
+
startproject_parser.add_argument(
|
|
37
|
+
"-f",
|
|
38
|
+
"--framework",
|
|
39
|
+
required=True,
|
|
40
|
+
choices=("django", "fastapi"),
|
|
41
|
+
help="Framework scaffold to generate.",
|
|
42
|
+
)
|
|
43
|
+
startproject_parser.add_argument(
|
|
44
|
+
"--force",
|
|
45
|
+
action="store_true",
|
|
46
|
+
help="Overwrite scaffold-managed files in a non-empty target directory.",
|
|
47
|
+
)
|
|
48
|
+
startproject_parser.add_argument(
|
|
49
|
+
"--dry-run",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Print planned actions without writing files.",
|
|
52
|
+
)
|
|
53
|
+
startproject_parser.add_argument(
|
|
54
|
+
"--verbose",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Print per-file actions during generation.",
|
|
57
|
+
)
|
|
58
|
+
startproject_parser.set_defaults(handler=handle_startproject)
|
|
59
|
+
|
|
60
|
+
return parser
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main(argv: list[str] | None = None) -> int:
|
|
64
|
+
"""Run the CLI and return an exit code."""
|
|
65
|
+
|
|
66
|
+
parser = build_parser()
|
|
67
|
+
args = parser.parse_args(argv)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
return args.handler(args)
|
|
71
|
+
except TsesError as exc:
|
|
72
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
73
|
+
return 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def run() -> None:
|
|
77
|
+
"""Run the CLI as a console-script entry point."""
|
|
78
|
+
|
|
79
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI command handlers for TSES."""
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Implementation of the `tses startproject` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from tses.core.filesystem import PlannedAction
|
|
8
|
+
from tses.core.generator import GenerationResult, generate_scaffold
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def handle_startproject(args) -> int:
|
|
12
|
+
"""Generate the requested scaffold and print user-facing output."""
|
|
13
|
+
|
|
14
|
+
result = generate_scaffold(
|
|
15
|
+
framework=args.framework,
|
|
16
|
+
target=Path(args.target),
|
|
17
|
+
force=args.force,
|
|
18
|
+
dry_run=args.dry_run,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if args.dry_run:
|
|
22
|
+
print(f"Dry run for {result.framework} scaffold")
|
|
23
|
+
print(f"Target: {result.target_dir}")
|
|
24
|
+
for action in result.actions:
|
|
25
|
+
_print_action(action, prefix="Would")
|
|
26
|
+
print(
|
|
27
|
+
f"Would create {result.created_count} files and overwrite "
|
|
28
|
+
f"{result.overwritten_count} files."
|
|
29
|
+
)
|
|
30
|
+
return 0
|
|
31
|
+
|
|
32
|
+
if args.verbose:
|
|
33
|
+
for action in result.actions:
|
|
34
|
+
_print_action(action)
|
|
35
|
+
|
|
36
|
+
print("Project generated successfully.")
|
|
37
|
+
print(f"Framework: {result.framework}")
|
|
38
|
+
print(f"Target: {result.target_dir}")
|
|
39
|
+
print(f"Project name: {result.metadata.project_name}")
|
|
40
|
+
print(f"Force mode: {'yes' if args.force else 'no'}")
|
|
41
|
+
print(f"Created files: {result.created_count}")
|
|
42
|
+
print(f"Overwritten files: {result.overwritten_count}")
|
|
43
|
+
print("Next steps:")
|
|
44
|
+
for step in _build_next_steps(result):
|
|
45
|
+
print(f" {step}")
|
|
46
|
+
return 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _print_action(action: PlannedAction, prefix: str | None = None) -> None:
|
|
50
|
+
"""Print one planned or completed file action."""
|
|
51
|
+
|
|
52
|
+
verb = action.kind if prefix is None else f"{prefix} {action.kind}"
|
|
53
|
+
print(f"{verb:>16} {action.relative_path.as_posix()}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _build_next_steps(result: GenerationResult) -> list[str]:
|
|
57
|
+
"""Return framework-specific follow-up steps for the user."""
|
|
58
|
+
|
|
59
|
+
steps: list[str] = []
|
|
60
|
+
if result.original_target != Path("."):
|
|
61
|
+
steps.append(f"cd {result.target_dir}")
|
|
62
|
+
|
|
63
|
+
env_example = result.target_dir / ".env.example"
|
|
64
|
+
env_file = result.target_dir / ".env"
|
|
65
|
+
if env_example.exists() and not env_file.exists():
|
|
66
|
+
steps.append("cp .env.example .env")
|
|
67
|
+
|
|
68
|
+
if result.framework == "django":
|
|
69
|
+
steps.append("docker compose up --build")
|
|
70
|
+
steps.append("python manage.py migrate")
|
|
71
|
+
else:
|
|
72
|
+
steps.append("docker compose up --build")
|
|
73
|
+
steps.append("alembic upgrade head")
|
|
74
|
+
|
|
75
|
+
return steps
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Core generation utilities for TSES."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Custom exceptions raised by the TSES scaffold generator."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class TsesError(Exception):
|
|
5
|
+
"""Base exception for user-facing generator errors."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class UnsupportedFrameworkError(TsesError):
|
|
9
|
+
"""Raised when a framework choice is not supported."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnsafeTargetError(TsesError):
|
|
13
|
+
"""Raised when generation would write into an unsafe target directory."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GenerationError(TsesError):
|
|
17
|
+
"""Raised when file generation cannot complete safely."""
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Filesystem planning helpers for scaffold generation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Iterator
|
|
8
|
+
|
|
9
|
+
from importlib.abc import Traversable
|
|
10
|
+
|
|
11
|
+
from tses.core.errors import GenerationError, UnsafeTargetError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class PlannedAction:
|
|
16
|
+
"""One file action required to realize a scaffold generation request."""
|
|
17
|
+
|
|
18
|
+
kind: str
|
|
19
|
+
relative_path: Path
|
|
20
|
+
source: Traversable
|
|
21
|
+
destination: Path
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def prepare_target_directory(target_dir: Path, *, force: bool, dry_run: bool) -> None:
|
|
25
|
+
"""Validate the target directory and create it when appropriate."""
|
|
26
|
+
|
|
27
|
+
if target_dir.exists() and not target_dir.is_dir():
|
|
28
|
+
raise UnsafeTargetError(f"Target path is not a directory: {target_dir}")
|
|
29
|
+
|
|
30
|
+
if target_dir.exists():
|
|
31
|
+
if any(target_dir.iterdir()) and not force:
|
|
32
|
+
raise UnsafeTargetError(
|
|
33
|
+
"Target directory is not empty. Re-run with --force to overwrite "
|
|
34
|
+
"scaffold-managed files."
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
if not dry_run:
|
|
39
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def plan_actions(template_root: Traversable, target_dir: Path) -> list[PlannedAction]:
|
|
43
|
+
"""Plan the file actions required to copy a scaffold into the target."""
|
|
44
|
+
|
|
45
|
+
actions: list[PlannedAction] = []
|
|
46
|
+
for source, relative_path in iter_template_files(template_root):
|
|
47
|
+
destination = target_dir / relative_path
|
|
48
|
+
_validate_parent_paths(target_dir=target_dir, destination=destination)
|
|
49
|
+
|
|
50
|
+
if destination.exists() and destination.is_dir():
|
|
51
|
+
raise GenerationError(
|
|
52
|
+
f"Cannot overwrite directory with file: {destination}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
action_kind = "overwrite" if destination.exists() else "create"
|
|
56
|
+
actions.append(
|
|
57
|
+
PlannedAction(
|
|
58
|
+
kind=action_kind,
|
|
59
|
+
relative_path=relative_path,
|
|
60
|
+
source=source,
|
|
61
|
+
destination=destination,
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return actions
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def iter_template_files(
|
|
69
|
+
root: Traversable,
|
|
70
|
+
relative_path: Path = Path(),
|
|
71
|
+
) -> Iterator[tuple[Traversable, Path]]:
|
|
72
|
+
"""Yield every file under a template root with a stable relative path."""
|
|
73
|
+
|
|
74
|
+
for child in sorted(root.iterdir(), key=lambda entry: entry.name):
|
|
75
|
+
child_relative_path = relative_path / child.name
|
|
76
|
+
if child.is_dir():
|
|
77
|
+
yield from iter_template_files(child, child_relative_path)
|
|
78
|
+
elif child.is_file():
|
|
79
|
+
yield child, child_relative_path
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _validate_parent_paths(*, target_dir: Path, destination: Path) -> None:
|
|
83
|
+
"""Ensure no parent segment that should be a directory is already a file."""
|
|
84
|
+
|
|
85
|
+
current = destination.parent
|
|
86
|
+
while current != target_dir.parent:
|
|
87
|
+
if current.exists() and not current.is_dir():
|
|
88
|
+
raise GenerationError(f"Path segment is a file, not a directory: {current}")
|
|
89
|
+
if current == target_dir:
|
|
90
|
+
break
|
|
91
|
+
current = current.parent
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""High-level scaffold generation orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from importlib import resources
|
|
7
|
+
from importlib.abc import Traversable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from tses.core.errors import GenerationError, UnsupportedFrameworkError
|
|
11
|
+
from tses.core.filesystem import PlannedAction, plan_actions, prepare_target_directory
|
|
12
|
+
from tses.core.metadata import GenerationMetadata, build_generation_metadata
|
|
13
|
+
from tses.core.renderer import apply_action
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
SUPPORTED_FRAMEWORKS = {"django", "fastapi"}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class GenerationResult:
|
|
21
|
+
"""Summary of one scaffold generation run."""
|
|
22
|
+
|
|
23
|
+
framework: str
|
|
24
|
+
original_target: Path
|
|
25
|
+
target_dir: Path
|
|
26
|
+
metadata: GenerationMetadata
|
|
27
|
+
actions: list[PlannedAction]
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def created_count(self) -> int:
|
|
31
|
+
"""Return the number of created files."""
|
|
32
|
+
|
|
33
|
+
return sum(1 for action in self.actions if action.kind == "create")
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def overwritten_count(self) -> int:
|
|
37
|
+
"""Return the number of overwritten files."""
|
|
38
|
+
|
|
39
|
+
return sum(1 for action in self.actions if action.kind == "overwrite")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def generate_scaffold(
|
|
43
|
+
*,
|
|
44
|
+
framework: str,
|
|
45
|
+
target: Path,
|
|
46
|
+
force: bool = False,
|
|
47
|
+
dry_run: bool = False,
|
|
48
|
+
) -> GenerationResult:
|
|
49
|
+
"""Generate a vendored framework scaffold into the target directory."""
|
|
50
|
+
|
|
51
|
+
normalized_framework = framework.strip().lower()
|
|
52
|
+
template_root = get_template_root(normalized_framework)
|
|
53
|
+
|
|
54
|
+
target_dir = target.expanduser().resolve()
|
|
55
|
+
metadata = build_generation_metadata(
|
|
56
|
+
framework=normalized_framework,
|
|
57
|
+
target_dir=target_dir,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
prepare_target_directory(target_dir, force=force, dry_run=dry_run)
|
|
61
|
+
actions = plan_actions(template_root, target_dir)
|
|
62
|
+
|
|
63
|
+
if not dry_run:
|
|
64
|
+
for action in actions:
|
|
65
|
+
apply_action(action, metadata)
|
|
66
|
+
|
|
67
|
+
return GenerationResult(
|
|
68
|
+
framework=normalized_framework,
|
|
69
|
+
original_target=target,
|
|
70
|
+
target_dir=target_dir,
|
|
71
|
+
metadata=metadata,
|
|
72
|
+
actions=actions,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_template_root(framework: str) -> Traversable:
|
|
77
|
+
"""Return the vendored template root for a supported framework."""
|
|
78
|
+
|
|
79
|
+
if framework not in SUPPORTED_FRAMEWORKS:
|
|
80
|
+
raise UnsupportedFrameworkError(
|
|
81
|
+
f"Unsupported framework '{framework}'. Expected one of: django, fastapi."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
template_root = resources.files("tses.templates").joinpath(framework)
|
|
85
|
+
if not template_root.exists() or not template_root.is_dir():
|
|
86
|
+
raise GenerationError(
|
|
87
|
+
f"Vendored template for framework '{framework}' is missing. "
|
|
88
|
+
"Run tools/sync_scaffolds.py to refresh scaffold snapshots."
|
|
89
|
+
)
|
|
90
|
+
return template_root
|