tses 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. tses/__init__.py +3 -0
  2. tses/__main__.py +7 -0
  3. tses/cli.py +79 -0
  4. tses/commands/__init__.py +1 -0
  5. tses/commands/startproject.py +75 -0
  6. tses/core/__init__.py +1 -0
  7. tses/core/errors.py +17 -0
  8. tses/core/filesystem.py +91 -0
  9. tses/core/generator.py +90 -0
  10. tses/core/headers.py +78 -0
  11. tses/core/metadata.py +55 -0
  12. tses/core/renderer.py +48 -0
  13. tses/templates/__init__.py +1 -0
  14. tses/templates/django/.docker/Dockerfile +22 -0
  15. tses/templates/django/.docker/entrypoint.sh +15 -0
  16. tses/templates/django/.dockerignore +24 -0
  17. tses/templates/django/.env.example +32 -0
  18. tses/templates/django/.github/workflows/deploy.yaml +42 -0
  19. tses/templates/django/.gitignore +27 -0
  20. tses/templates/django/README.md +442 -0
  21. tses/templates/django/api/__init__.py +0 -0
  22. tses/templates/django/api/authentication/__init__.py +1 -0
  23. tses/templates/django/api/authentication/utils/__init__.py +3 -0
  24. tses/templates/django/api/authentication/utils/base_auth.py +35 -0
  25. tses/templates/django/api/authorization/__init__.py +0 -0
  26. tses/templates/django/api/authorization/admin.py +3 -0
  27. tses/templates/django/api/authorization/apps.py +6 -0
  28. tses/templates/django/api/authorization/migrations/__init__.py +0 -0
  29. tses/templates/django/api/authorization/models/__init__.py +0 -0
  30. tses/templates/django/api/authorization/serializers/__init__.py +0 -0
  31. tses/templates/django/api/authorization/tests.py +3 -0
  32. tses/templates/django/api/authorization/views/__init__.py +0 -0
  33. tses/templates/django/api/notification/__init__.py +0 -0
  34. tses/templates/django/api/notification/admin.py +3 -0
  35. tses/templates/django/api/notification/apps.py +6 -0
  36. tses/templates/django/api/notification/enums.py +26 -0
  37. tses/templates/django/api/notification/migrations/0001_initial.py +34 -0
  38. tses/templates/django/api/notification/migrations/0002_initial.py +31 -0
  39. tses/templates/django/api/notification/migrations/__init__.py +0 -0
  40. tses/templates/django/api/notification/models.py +244 -0
  41. tses/templates/django/api/notification/serializers.py +0 -0
  42. tses/templates/django/api/notification/tests.py +3 -0
  43. tses/templates/django/api/notification/views.py +0 -0
  44. tses/templates/django/api/users/__init__.py +0 -0
  45. tses/templates/django/api/users/admin.py +3 -0
  46. tses/templates/django/api/users/apps.py +6 -0
  47. tses/templates/django/api/users/enums.py +31 -0
  48. tses/templates/django/api/users/migrations/0001_initial.py +72 -0
  49. tses/templates/django/api/users/migrations/0002_alter_user_created_by_alter_user_updated_by.py +25 -0
  50. tses/templates/django/api/users/migrations/__init__.py +0 -0
  51. tses/templates/django/api/users/models/__init__.py +7 -0
  52. tses/templates/django/api/users/models/activity_log.py +123 -0
  53. tses/templates/django/api/users/models/manager.py +37 -0
  54. tses/templates/django/api/users/models/user.py +37 -0
  55. tses/templates/django/api/users/serializers/__init__.py +0 -0
  56. tses/templates/django/api/users/tests.py +3 -0
  57. tses/templates/django/api/users/views/__init__.py +0 -0
  58. tses/templates/django/config/__init__.py +3 -0
  59. tses/templates/django/config/asgi.py +16 -0
  60. tses/templates/django/config/celery.py +19 -0
  61. tses/templates/django/config/settings/__init__.py +25 -0
  62. tses/templates/django/config/settings/app_registry.py +26 -0
  63. tses/templates/django/config/settings/celery.py +12 -0
  64. tses/templates/django/config/settings/commons.py +115 -0
  65. tses/templates/django/config/settings/cors.py +12 -0
  66. tses/templates/django/config/settings/db.py +12 -0
  67. tses/templates/django/config/settings/drf.py +38 -0
  68. tses/templates/django/config/settings/exception.py +6 -0
  69. tses/templates/django/config/settings/jwt.py +31 -0
  70. tses/templates/django/config/settings/middleware.py +11 -0
  71. tses/templates/django/config/settings/s3.py +0 -0
  72. tses/templates/django/config/settings/smtp.py +16 -0
  73. tses/templates/django/config/urls.py +22 -0
  74. tses/templates/django/config/wsgi.py +16 -0
  75. tses/templates/django/docker-compose.yaml +52 -0
  76. tses/templates/django/includes/__init__.py +0 -0
  77. tses/templates/django/includes/helpers/__init__.py +11 -0
  78. tses/templates/django/includes/helpers/formatter.py +25 -0
  79. tses/templates/django/includes/helpers/models.py +73 -0
  80. tses/templates/django/includes/helpers/pagination.py +217 -0
  81. tses/templates/django/includes/third_party/__init__.py +0 -0
  82. tses/templates/django/includes/third_party/exceptions.py +42 -0
  83. tses/templates/django/includes/third_party/processor.py +59 -0
  84. tses/templates/django/manage.py +22 -0
  85. tses/templates/django/requirements.txt +11 -0
  86. tses/templates/fastapi/.docker/Dockerfile +22 -0
  87. tses/templates/fastapi/.docker/entrypoint.sh +8 -0
  88. tses/templates/fastapi/.dockerignore +10 -0
  89. tses/templates/fastapi/.env.example +29 -0
  90. tses/templates/fastapi/.github/workflows/deploy.yaml +42 -0
  91. tses/templates/fastapi/README.md +516 -0
  92. tses/templates/fastapi/alembic/env.py +78 -0
  93. tses/templates/fastapi/alembic/script.py.mako +20 -0
  94. tses/templates/fastapi/alembic/versions/0001_initial.py +73 -0
  95. tses/templates/fastapi/alembic/versions/__init__.py +1 -0
  96. tses/templates/fastapi/alembic.ini +36 -0
  97. tses/templates/fastapi/api/__init__.py +0 -0
  98. tses/templates/fastapi/api/auth/__init__.py +24 -0
  99. tses/templates/fastapi/api/auth/base_auth.py +35 -0
  100. tses/templates/fastapi/api/auth/schemas.py +15 -0
  101. tses/templates/fastapi/api/auth/services/__init__.py +8 -0
  102. tses/templates/fastapi/api/auth/services/auth.py +94 -0
  103. tses/templates/fastapi/api/auth/services/token.py +86 -0
  104. tses/templates/fastapi/api/health/__init__.py +0 -0
  105. tses/templates/fastapi/api/health/routers.py +44 -0
  106. tses/templates/fastapi/api/health/schemas.py +24 -0
  107. tses/templates/fastapi/api/health/service.py +59 -0
  108. tses/templates/fastapi/api/users/__init__.py +1 -0
  109. tses/templates/fastapi/api/users/models/__init__.py +3 -0
  110. tses/templates/fastapi/api/users/models/users.py +67 -0
  111. tses/templates/fastapi/api/users/routers.py +12 -0
  112. tses/templates/fastapi/api/users/schemas.py +24 -0
  113. tses/templates/fastapi/config/__init__.py +0 -0
  114. tses/templates/fastapi/config/db.py +99 -0
  115. tses/templates/fastapi/config/settings.py +161 -0
  116. tses/templates/fastapi/docker-compose.yaml +20 -0
  117. tses/templates/fastapi/includes/__init__.py +1 -0
  118. tses/templates/fastapi/includes/errors.py +57 -0
  119. tses/templates/fastapi/includes/exception_handlers.py +155 -0
  120. tses/templates/fastapi/includes/libs/__init__.py +4 -0
  121. tses/templates/fastapi/includes/libs/s3.py +348 -0
  122. tses/templates/fastapi/includes/middleware/__init__.py +3 -0
  123. tses/templates/fastapi/includes/middleware/error_handler.py +30 -0
  124. tses/templates/fastapi/includes/models.py +170 -0
  125. tses/templates/fastapi/includes/pagination.py +148 -0
  126. tses/templates/fastapi/includes/schemas.py +52 -0
  127. tses/templates/fastapi/includes/third_party/__init__.py +0 -0
  128. tses/templates/fastapi/includes/third_party/exceptions.py +42 -0
  129. tses/templates/fastapi/includes/third_party/processor.py +59 -0
  130. tses/templates/fastapi/main.py +72 -0
  131. tses/templates/fastapi/requirements.txt +13 -0
  132. tses-0.1.0.dist-info/METADATA +79 -0
  133. tses-0.1.0.dist-info/RECORD +136 -0
  134. tses-0.1.0.dist-info/WHEEL +4 -0
  135. tses-0.1.0.dist-info/entry_points.txt +2 -0
  136. tses-0.1.0.dist-info/licenses/LICENSE +7 -0
tses/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """TSES scaffold generator package."""
2
+
3
+ __version__ = "0.1.0"
tses/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Module entry point for `python -m tses`."""
2
+
3
+ from tses.cli import run
4
+
5
+
6
+ if __name__ == "__main__":
7
+ run()
tses/cli.py ADDED
@@ -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
tses/core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Core generation utilities for TSES."""
tses/core/errors.py ADDED
@@ -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
tses/core/generator.py ADDED
@@ -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
tses/core/headers.py ADDED
@@ -0,0 +1,78 @@
1
+ """Generated-file header insertion helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from tses.core.metadata import GenerationMetadata
8
+
9
+
10
+ HASH_COMMENT_FILENAMES = {
11
+ ".dockerignore",
12
+ ".env",
13
+ ".env.example",
14
+ ".gitignore",
15
+ "Dockerfile",
16
+ "Makefile",
17
+ }
18
+ HASH_COMMENT_SUFFIXES = {
19
+ ".conf",
20
+ ".cfg",
21
+ ".env",
22
+ ".ini",
23
+ ".py",
24
+ ".pyi",
25
+ ".requirements",
26
+ ".sh",
27
+ ".sql",
28
+ ".toml",
29
+ ".yaml",
30
+ ".yml",
31
+ }
32
+ BLOCK_COMMENT_SUFFIXES = {".css", ".js", ".jsx", ".ts", ".tsx"}
33
+ HTML_COMMENT_SUFFIXES = {".html", ".htm", ".markdown", ".md", ".svg", ".xml"}
34
+
35
+
36
+ def inject_generated_header(
37
+ content: str,
38
+ relative_path: Path,
39
+ metadata: GenerationMetadata,
40
+ ) -> str:
41
+ """Inject a generated-file header when the file type supports comments."""
42
+
43
+ comment = _build_comment(relative_path, metadata.header_text)
44
+ if comment is None:
45
+ return content
46
+
47
+ insertion = f"{comment}\n"
48
+ if content.startswith("#!"):
49
+ return _insert_after_first_line(content, insertion)
50
+ if content.startswith("<?xml"):
51
+ return _insert_after_first_line(content, insertion)
52
+ return f"{insertion}{content}"
53
+
54
+
55
+ def _build_comment(relative_path: Path, text: str) -> str | None:
56
+ """Return the correctly formatted comment string for a file path."""
57
+
58
+ name = relative_path.name
59
+ suffixes = set(relative_path.suffixes)
60
+
61
+ if name in HASH_COMMENT_FILENAMES or suffixes & HASH_COMMENT_SUFFIXES:
62
+ return f"# {text}"
63
+ if suffixes & BLOCK_COMMENT_SUFFIXES:
64
+ return f"/* {text} */"
65
+ if suffixes & HTML_COMMENT_SUFFIXES:
66
+ return f"<!-- {text} -->"
67
+ if name.startswith("requirements"):
68
+ return f"# {text}"
69
+ return None
70
+
71
+
72
+ def _insert_after_first_line(content: str, insertion: str) -> str:
73
+ """Insert text immediately after the first line of a file."""
74
+
75
+ newline_index = content.find("\n")
76
+ if newline_index == -1:
77
+ return f"{content}\n{insertion}"
78
+ return f"{content[: newline_index + 1]}{insertion}{content[newline_index + 1:]}"
tses/core/metadata.py ADDED
@@ -0,0 +1,55 @@
1
+ """Metadata helpers for scaffold generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timezone
7
+ from pathlib import Path
8
+ import re
9
+
10
+ from tses.core.errors import GenerationError
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class GenerationMetadata:
15
+ """Context values associated with one generation run."""
16
+
17
+ framework: str
18
+ project_name: str
19
+ project_slug: str
20
+ target_dir: Path
21
+ generated_at: str
22
+
23
+ @property
24
+ def header_text(self) -> str:
25
+ """Return the canonical generated-file header text."""
26
+
27
+ return f"Generated by Tses {self.generated_at}"
28
+
29
+
30
+ def build_generation_metadata(*, framework: str, target_dir: Path) -> GenerationMetadata:
31
+ """Build normalized metadata for a generation request."""
32
+
33
+ resolved_target = target_dir.resolve()
34
+ project_name = resolved_target.name.strip()
35
+ if not project_name:
36
+ raise GenerationError("Could not derive a project name from the target path.")
37
+
38
+ project_slug = slugify(project_name)
39
+ generated_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace(
40
+ "+00:00", "Z"
41
+ )
42
+ return GenerationMetadata(
43
+ framework=framework,
44
+ project_name=project_name,
45
+ project_slug=project_slug,
46
+ target_dir=resolved_target,
47
+ generated_at=generated_at,
48
+ )
49
+
50
+
51
+ def slugify(value: str) -> str:
52
+ """Normalize a directory-derived value into a simple slug."""
53
+
54
+ normalized = re.sub(r"[^a-zA-Z0-9]+", "-", value.strip().lower()).strip("-")
55
+ return normalized or "project"
tses/core/renderer.py ADDED
@@ -0,0 +1,48 @@
1
+ """Rendering and file-write helpers for scaffold generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from tses.core.filesystem import PlannedAction
8
+ from tses.core.headers import inject_generated_header
9
+ from tses.core.metadata import GenerationMetadata
10
+
11
+
12
+ def apply_action(action: PlannedAction, metadata: GenerationMetadata) -> None:
13
+ """Render one planned file action to disk."""
14
+
15
+ raw_bytes = action.source.read_bytes()
16
+ rendered_bytes = render_bytes(
17
+ raw_bytes=raw_bytes,
18
+ relative_path=action.relative_path,
19
+ metadata=metadata,
20
+ )
21
+
22
+ action.destination.parent.mkdir(parents=True, exist_ok=True)
23
+ action.destination.write_bytes(rendered_bytes)
24
+ _apply_permissions(action.destination, action.relative_path)
25
+
26
+
27
+ def render_bytes(
28
+ *,
29
+ raw_bytes: bytes,
30
+ relative_path: Path,
31
+ metadata: GenerationMetadata,
32
+ ) -> bytes:
33
+ """Render bytes for a generated file, injecting headers where allowed."""
34
+
35
+ try:
36
+ text = raw_bytes.decode("utf-8")
37
+ except UnicodeDecodeError:
38
+ return raw_bytes
39
+
40
+ rendered_text = inject_generated_header(text, relative_path, metadata)
41
+ return rendered_text.encode("utf-8")
42
+
43
+
44
+ def _apply_permissions(destination: Path, relative_path: Path) -> None:
45
+ """Apply deterministic executable permissions for generated scripts."""
46
+
47
+ if relative_path.suffix == ".sh":
48
+ destination.chmod(destination.stat().st_mode | 0o111)
@@ -0,0 +1 @@
1
+ """Vendored scaffold templates packaged with TSES."""
@@ -0,0 +1,22 @@
1
+ FROM python:3.12-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1 \
4
+ PYTHONUNBUFFERED=1 \
5
+ DJANGO_SETTINGS_MODULE=config.settings
6
+
7
+ WORKDIR /app
8
+
9
+ COPY requirements.txt /app/requirements.txt
10
+
11
+ RUN pip install --upgrade pip \
12
+ && pip install -r /app/requirements.txt
13
+
14
+ COPY . /app
15
+
16
+ RUN chmod +x /app/.docker/entrypoint.sh
17
+
18
+ EXPOSE 8000
19
+
20
+ ENTRYPOINT ["/bin/sh", "/app/.docker/entrypoint.sh"]
21
+
22
+ CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
@@ -0,0 +1,15 @@
1
+ #!/bin/sh
2
+ set -e
3
+
4
+ python manage.py check
5
+
6
+ if [ "${DJANGO_RUN_MIGRATIONS:-1}" = "1" ]; then
7
+ python manage.py migrate --noinput
8
+ fi
9
+
10
+ # static collection opt-in because project may not define STATIC_ROOT yet.
11
+ if [ "${DJANGO_COLLECTSTATIC:-0}" = "1" ]; then
12
+ python manage.py collectstatic --noinput
13
+ fi
14
+
15
+ exec "$@"
@@ -0,0 +1,24 @@
1
+ .git/
2
+ .agents/
3
+ .codex/
4
+
5
+ .env
6
+ .env.*
7
+ !.env.example
8
+
9
+ __pycache__/
10
+ *.py[cod]
11
+ *.sqlite3
12
+ .python-version
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ .mypy_cache/
18
+ .pytest_cache/
19
+ .ruff_cache/
20
+ .coverage
21
+ htmlcov/
22
+
23
+ staticfiles/
24
+ media/
@@ -0,0 +1,32 @@
1
+ # Django
2
+ DJANGO_SETTINGS_MODULE=config.settings
3
+ SECRET_KEY=change-me
4
+ DEBUG=True
5
+ ALLOWED_HOSTS=localhost,127.0.0.1
6
+
7
+ # Database
8
+ DB_HOST=postgres
9
+ DB_PORT=5432
10
+ DB_NAME=postgres
11
+ DB_USER=postgres
12
+ DB_PASSWORD=postgres
13
+
14
+ # JWT
15
+ SIGNING_KEY=change-me
16
+
17
+ # Container startup
18
+ DJANGO_RUN_MIGRATIONS=1
19
+ DJANGO_COLLECTSTATIC=0
20
+
21
+ # Celery
22
+ CELERY_BROKER_URL=redis://redis:6379/0
23
+ CELERY_RESULT_BACKEND=redis://redis:6379/0
24
+
25
+ # Email
26
+ EMAIL_HOST=localhost
27
+ EMAIL_PORT=25
28
+ EMAIL_USE_SSL=False
29
+ EMAIL_USE_TLS=False
30
+ EMAIL_HOST_USER=
31
+ EMAIL_HOST_PASSWORD=
32
+ DEFAULT_FROM_EMAIL=no-reply@example.com