swiftpy-framework 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.
- swiftpy/.gitignore +0 -0
- swiftpy/__init__.py +572 -0
- swiftpy/admin/__init__.py +50 -0
- swiftpy/admin/auth.py +522 -0
- swiftpy/admin/registry.py +567 -0
- swiftpy/admin/views.py +1583 -0
- swiftpy/ai/__init__.py +199 -0
- swiftpy/ai/_deterministic.py +40 -0
- swiftpy/ai/_fake.py +604 -0
- swiftpy/ai/agents/__init__.py +30 -0
- swiftpy/ai/agents/base_agent.py +506 -0
- swiftpy/ai/agents/memory.py +864 -0
- swiftpy/ai/client.py +1011 -0
- swiftpy/ai/conversation.py +171 -0
- swiftpy/ai/embeddings.py +321 -0
- swiftpy/ai/exceptions.py +75 -0
- swiftpy/ai/history_strategies.py +157 -0
- swiftpy/ai/middleware.py +918 -0
- swiftpy/ai/observability/__init__.py +28 -0
- swiftpy/ai/observability/drivers.py +763 -0
- swiftpy/ai/observability/observer.py +364 -0
- swiftpy/ai/pool.py +102 -0
- swiftpy/ai/pricing.py +101 -0
- swiftpy/ai/prompts/__init__.py +6 -0
- swiftpy/ai/prompts/loader.py +343 -0
- swiftpy/ai/providers/__init__.py +22 -0
- swiftpy/ai/providers/anthropic_provider.py +353 -0
- swiftpy/ai/providers/base.py +273 -0
- swiftpy/ai/providers/openai_provider.py +275 -0
- swiftpy/ai/rag/__init__.py +53 -0
- swiftpy/ai/rag/context_formatter.py +263 -0
- swiftpy/ai/rag/pipeline_builder.py +544 -0
- swiftpy/ai/rag/reranker.py +484 -0
- swiftpy/ai/rag/result.py +78 -0
- swiftpy/ai/rag/retriever.py +315 -0
- swiftpy/ai/redis_store.py +227 -0
- swiftpy/ai/streaming.py +115 -0
- swiftpy/ai/tools/__init__.py +22 -0
- swiftpy/ai/tools/base_tool.py +489 -0
- swiftpy/ai/tools/first_party.py +384 -0
- swiftpy/ai/tools/tool_loop.py +417 -0
- swiftpy/ai/vectorstore/__init__.py +46 -0
- swiftpy/ai/vectorstore/base.py +542 -0
- swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
- swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
- swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
- swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
- swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
- swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
- swiftpy/auth/__init__.py +50 -0
- swiftpy/auth/api_key_guard.py +544 -0
- swiftpy/auth/auth_manager.py +259 -0
- swiftpy/auth/exceptions.py +71 -0
- swiftpy/auth/jwt_guard.py +230 -0
- swiftpy/auth/jwt_issuer.py +658 -0
- swiftpy/auth/token_blacklist.py +300 -0
- swiftpy/cache/__init__.py +14 -0
- swiftpy/cache/cache.py +251 -0
- swiftpy/cache/driver.py +216 -0
- swiftpy/cache/drivers/memory.py +199 -0
- swiftpy/cache/drivers/redis.py +421 -0
- swiftpy/console/__init__.py +0 -0
- swiftpy/console/cli.py +68 -0
- swiftpy/console/commands/__init__.py +0 -0
- swiftpy/console/commands/cache.py +164 -0
- swiftpy/console/commands/make.py +935 -0
- swiftpy/console/commands/migrate.py +280 -0
- swiftpy/console/commands/new.py +1806 -0
- swiftpy/console/commands/queue.py +306 -0
- swiftpy/console/commands/seed.py +252 -0
- swiftpy/console/commands/start.py +171 -0
- swiftpy/console/commands/storage.py +104 -0
- swiftpy/console/loader.py +21 -0
- swiftpy/console/stubs/__init__.py +26 -0
- swiftpy/console/stubs/controller.stub +14 -0
- swiftpy/console/stubs/event.stub +14 -0
- swiftpy/console/stubs/job.stub +22 -0
- swiftpy/console/stubs/listener.stub +14 -0
- swiftpy/console/stubs/mail.stub +20 -0
- swiftpy/console/stubs/middleware.stub +16 -0
- swiftpy/console/stubs/migration.alter.stub +17 -0
- swiftpy/console/stubs/migration.api_keys.stub +22 -0
- swiftpy/console/stubs/migration.blank.stub +11 -0
- swiftpy/console/stubs/migration.create.stub +15 -0
- swiftpy/console/stubs/migration.stub +11 -0
- swiftpy/console/stubs/migration.update.stub +17 -0
- swiftpy/console/stubs/model.stub +17 -0
- swiftpy/console/stubs/provider.stub +18 -0
- swiftpy/console/stubs/repository.stub +10 -0
- swiftpy/console/stubs/request.stub +16 -0
- swiftpy/console/stubs/resource.stub +21 -0
- swiftpy/console/stubs/schema.stub +12 -0
- swiftpy/console/stubs/seeder.stub +14 -0
- swiftpy/console/stubs/service.stub +10 -0
- swiftpy/core/__init__.py +0 -0
- swiftpy/core/application.py +1306 -0
- swiftpy/core/bootstrap.py +30 -0
- swiftpy/core/config.py +432 -0
- swiftpy/core/container.py +596 -0
- swiftpy/core/container_exceptions.py +18 -0
- swiftpy/core/context.py +148 -0
- swiftpy/core/exceptions.py +352 -0
- swiftpy/core/providers/base.py +35 -0
- swiftpy/database/__init__.py +41 -0
- swiftpy/database/exceptions.py +42 -0
- swiftpy/database/identifiers.py +105 -0
- swiftpy/database/migrations/__init__.py +36 -0
- swiftpy/database/migrations/blueprint.py +431 -0
- swiftpy/database/migrations/lock.py +104 -0
- swiftpy/database/migrations/migration.py +22 -0
- swiftpy/database/migrations/migrator.py +507 -0
- swiftpy/database/migrations/schema.py +435 -0
- swiftpy/database/orm/__init__.py +33 -0
- swiftpy/database/orm/fields.py +92 -0
- swiftpy/database/orm/model.py +2034 -0
- swiftpy/database/orm/registry.py +33 -0
- swiftpy/database/orm/relationships.py +836 -0
- swiftpy/database/pagination/__init__.py +16 -0
- swiftpy/database/pagination/contracts.py +60 -0
- swiftpy/database/pagination/paginator.py +112 -0
- swiftpy/database/pagination/simple_paginator.py +97 -0
- swiftpy/database/pool.py +218 -0
- swiftpy/database/query_builder.py +1803 -0
- swiftpy/database/seeder.py +39 -0
- swiftpy/database/transaction.py +276 -0
- swiftpy/events/__init__.py +10 -0
- swiftpy/events/dispatcher.py +308 -0
- swiftpy/events/redis_bus.py +552 -0
- swiftpy/filesystem/__init__.py +227 -0
- swiftpy/filesystem/disk.py +378 -0
- swiftpy/filesystem/drivers/__init__.py +11 -0
- swiftpy/filesystem/drivers/base.py +156 -0
- swiftpy/filesystem/drivers/local.py +559 -0
- swiftpy/filesystem/drivers/s3.py +655 -0
- swiftpy/filesystem/events.py +37 -0
- swiftpy/filesystem/exceptions.py +127 -0
- swiftpy/filesystem/manager.py +138 -0
- swiftpy/filesystem/metadata.py +22 -0
- swiftpy/filesystem/paths.py +99 -0
- swiftpy/filesystem/service_provider.py +17 -0
- swiftpy/filesystem/uploads/__init__.py +13 -0
- swiftpy/filesystem/uploads/handlers.py +362 -0
- swiftpy/filesystem/uploads/uploaded_file.py +222 -0
- swiftpy/filesystem/uploads/validators.py +186 -0
- swiftpy/filesystem/visibility.py +22 -0
- swiftpy/helpers/__init__.py +42 -0
- swiftpy/helpers/helper.py +180 -0
- swiftpy/http/exceptions.py +41 -0
- swiftpy/http/middleware.py +307 -0
- swiftpy/http/request.py +375 -0
- swiftpy/http/response.py +442 -0
- swiftpy/http/router.py +1050 -0
- swiftpy/mail/__init__.py +58 -0
- swiftpy/mail/attachments.py +255 -0
- swiftpy/mail/config.py +71 -0
- swiftpy/mail/envelope.py +183 -0
- swiftpy/mail/exceptions.py +90 -0
- swiftpy/mail/facade.py +312 -0
- swiftpy/mail/mailable.py +53 -0
- swiftpy/mail/manager.py +240 -0
- swiftpy/mail/message.py +186 -0
- swiftpy/mail/models.py +19 -0
- swiftpy/mail/queue/__init__.py +5 -0
- swiftpy/mail/queue/job.py +279 -0
- swiftpy/mail/service_provider.py +19 -0
- swiftpy/mail/templates/__init__.py +5 -0
- swiftpy/mail/templates/renderer.py +173 -0
- swiftpy/mail/transports/__init__.py +21 -0
- swiftpy/mail/transports/api/__init__.py +13 -0
- swiftpy/mail/transports/api/base.py +194 -0
- swiftpy/mail/transports/api/mailgun.py +158 -0
- swiftpy/mail/transports/api/resend.py +156 -0
- swiftpy/mail/transports/api/sendgrid.py +172 -0
- swiftpy/mail/transports/base.py +24 -0
- swiftpy/mail/transports/console.py +91 -0
- swiftpy/mail/transports/sendmail.py +128 -0
- swiftpy/mail/transports/smtp.py +466 -0
- swiftpy/queue/__init__.py +13 -0
- swiftpy/queue/dispatcher.py +813 -0
- swiftpy/queue/job.py +541 -0
- swiftpy/queue/worker.py +556 -0
- swiftpy/support/__init__.py +12 -0
- swiftpy/support/keys.py +108 -0
- swiftpy/support/redis_config.py +223 -0
- swiftpy/support/strings.py +114 -0
- swiftpy/testing/__init__.py +20 -0
- swiftpy/testing/fakes/__init__.py +11 -0
- swiftpy/testing/fakes/ai_fake.py +21 -0
- swiftpy/testing/fakes/vectorstore_fake.py +194 -0
- swiftpy/testing/fixtures.py +124 -0
- swiftpy/testing/http_client.py +730 -0
- swiftpy/testing/test_case.py +126 -0
- swiftpy/validation/__init__.py +97 -0
- swiftpy/validation/exceptions.py +26 -0
- swiftpy/validation/form_request.py +302 -0
- swiftpy/validation/protocols.py +33 -0
- swiftpy/validation/rules/__init__.py +89 -0
- swiftpy/validation/rules/builtin.py +391 -0
- swiftpy/validation/rules/exists.py +86 -0
- swiftpy/validation/rules/registry.py +290 -0
- swiftpy/validation/rules/required.py +55 -0
- swiftpy/validation/rules/unique.py +138 -0
- swiftpy/validation/validator.py +612 -0
- swiftpy/websockets/__init__.py +19 -0
- swiftpy/websockets/connection.py +317 -0
- swiftpy/websockets/manager.py +271 -0
- swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
- swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
- swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
- swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
- swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from swiftpy.auth.api_key_guard import (
|
|
11
|
+
generate_api_key,
|
|
12
|
+
get_server_pepper,
|
|
13
|
+
hash_api_key,
|
|
14
|
+
)
|
|
15
|
+
from swiftpy.console.stubs import get_stub
|
|
16
|
+
from swiftpy.core.config import ConfigurationError
|
|
17
|
+
from swiftpy.database.identifiers import validate_identifier
|
|
18
|
+
from swiftpy.support.strings import pluralize, snake_case
|
|
19
|
+
|
|
20
|
+
_SEGMENT_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _pascal_case(snake_str: str) -> str:
|
|
24
|
+
"""Convert snake_case string to PascalCase class name."""
|
|
25
|
+
components = re.split(r"[^a-zA-Z0-9]+", snake_str)
|
|
26
|
+
return "".join(x.capitalize() for x in components if x)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _validate_and_split_name(
|
|
30
|
+
raw_name: str,
|
|
31
|
+
item_type: str = "File",
|
|
32
|
+
*,
|
|
33
|
+
allow_nested: bool = True,
|
|
34
|
+
) -> tuple[list[str], str, str]:
|
|
35
|
+
"""
|
|
36
|
+
Validate a generator name (optionally with nested '/' path segments) and return
|
|
37
|
+
(dir_segments, snake_name, class_name).
|
|
38
|
+
"""
|
|
39
|
+
trimmed = raw_name.strip()
|
|
40
|
+
if not trimmed:
|
|
41
|
+
raise SystemExit(f"Invalid {item_type.lower()} name: {raw_name!r}")
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
"\\" in trimmed
|
|
45
|
+
or trimmed.startswith("/")
|
|
46
|
+
or trimmed.endswith("/")
|
|
47
|
+
or "//" in trimmed
|
|
48
|
+
or Path(trimmed).is_absolute()
|
|
49
|
+
):
|
|
50
|
+
raise SystemExit(
|
|
51
|
+
f"Invalid {item_type.lower()} path {raw_name!r}: absolute or malformed paths are not allowed."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
segments = trimmed.split("/")
|
|
55
|
+
if not allow_nested and len(segments) > 1:
|
|
56
|
+
raise SystemExit(
|
|
57
|
+
f"Invalid {item_type.lower()} name {raw_name!r}: nested paths are not allowed."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
for seg in segments:
|
|
61
|
+
if seg in ("", ".", ".."):
|
|
62
|
+
raise SystemExit(
|
|
63
|
+
f"Invalid {item_type.lower()} path {raw_name!r}: path traversal is not allowed."
|
|
64
|
+
)
|
|
65
|
+
seg_snake = snake_case(seg)
|
|
66
|
+
if not _SEGMENT_PATTERN.fullmatch(seg) or not _SEGMENT_PATTERN.fullmatch(
|
|
67
|
+
seg_snake
|
|
68
|
+
):
|
|
69
|
+
raise SystemExit(
|
|
70
|
+
f"Invalid {item_type.lower()} name segment {seg!r} in {raw_name!r}. "
|
|
71
|
+
"Each segment must be a valid Python identifier starting with a letter or underscore."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
base_name = segments[-1]
|
|
75
|
+
snake_name = snake_case(base_name)
|
|
76
|
+
class_name = _pascal_case(snake_name)
|
|
77
|
+
if not class_name or not _SEGMENT_PATTERN.fullmatch(class_name):
|
|
78
|
+
raise SystemExit(
|
|
79
|
+
f"Invalid {item_type.lower()} class name {class_name!r} derived from {raw_name!r}."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
return segments[:-1], snake_name, class_name
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
_CREATE_PATTERNS = (
|
|
86
|
+
re.compile(r"^create_(\w+?)_table$"),
|
|
87
|
+
re.compile(r"^create_(\w+)$"),
|
|
88
|
+
)
|
|
89
|
+
_CHANGE_PATTERNS = (
|
|
90
|
+
re.compile(r".+_(?:to|from|in)_(\w+?)_table$"),
|
|
91
|
+
re.compile(r".+_(?:to|from|in)_(\w+)$"),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _guess_table(name: str) -> tuple[str | None, bool]:
|
|
96
|
+
"""Infer table name and whether migration creates a table from its name."""
|
|
97
|
+
for pattern in _CREATE_PATTERNS:
|
|
98
|
+
if match := pattern.match(name):
|
|
99
|
+
return match.group(1), True
|
|
100
|
+
for pattern in _CHANGE_PATTERNS:
|
|
101
|
+
if match := pattern.match(name):
|
|
102
|
+
return match.group(1), False
|
|
103
|
+
return None, False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
_CREATE_STUB = get_stub("migration.create")
|
|
107
|
+
_ALTER_STUB = get_stub("migration.alter")
|
|
108
|
+
_BLANK_STUB = get_stub("migration.blank")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _scaffold_file(
|
|
112
|
+
raw_name: str,
|
|
113
|
+
default_dir_rel: str,
|
|
114
|
+
stub_name: str,
|
|
115
|
+
override_path: str | None = None,
|
|
116
|
+
force: bool = False,
|
|
117
|
+
extra_replacements: dict[str, str] | None = None,
|
|
118
|
+
item_type: str = "File",
|
|
119
|
+
) -> Path:
|
|
120
|
+
"""
|
|
121
|
+
Generic scaffolding engine for SwiftPY generators.
|
|
122
|
+
Resolves class names, filenames, validates path containment, creates target directories,
|
|
123
|
+
and writes stub templates.
|
|
124
|
+
"""
|
|
125
|
+
dir_segments, snake_name, class_name = _validate_and_split_name(
|
|
126
|
+
raw_name,
|
|
127
|
+
item_type=item_type,
|
|
128
|
+
allow_nested=True,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
base_dir = Path(override_path) if override_path else Path.cwd() / default_dir_rel
|
|
132
|
+
resolved_base = base_dir.resolve()
|
|
133
|
+
target_dir = base_dir.joinpath(*dir_segments) if dir_segments else base_dir
|
|
134
|
+
filename = f"{snake_name}.py"
|
|
135
|
+
filepath = target_dir / filename
|
|
136
|
+
|
|
137
|
+
resolved_target_dir = target_dir.resolve()
|
|
138
|
+
resolved_filepath = filepath.resolve()
|
|
139
|
+
if not resolved_target_dir.is_relative_to(
|
|
140
|
+
resolved_base
|
|
141
|
+
) or not resolved_filepath.is_relative_to(resolved_base):
|
|
142
|
+
raise SystemExit(
|
|
143
|
+
f"Invalid {item_type.lower()} path {raw_name!r}: target escapes base directory '{resolved_base}'."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
|
|
148
|
+
# Re-verify containment after directory creation
|
|
149
|
+
if not filepath.resolve().is_relative_to(resolved_base) or filepath.is_symlink():
|
|
150
|
+
raise SystemExit(
|
|
151
|
+
f"Invalid {item_type.lower()} path {raw_name!r}: target escapes base directory '{resolved_base}'."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if filepath.exists() and not force:
|
|
155
|
+
raise SystemExit(f"{item_type} already exists: {filepath}. Use --force to overwrite.")
|
|
156
|
+
|
|
157
|
+
stub = get_stub(stub_name)
|
|
158
|
+
content = stub.replace("{class_name}", class_name)
|
|
159
|
+
if extra_replacements:
|
|
160
|
+
for k, v in extra_replacements.items():
|
|
161
|
+
content = content.replace(f"{{{k}}}", v)
|
|
162
|
+
|
|
163
|
+
filepath.write_text(content, encoding="utf-8")
|
|
164
|
+
|
|
165
|
+
display_name = str(filepath.relative_to(Path.cwd())) if filepath.is_relative_to(Path.cwd()) else str(filepath)
|
|
166
|
+
if sys.stdout.isatty() and "NO_COLOR" not in os.environ:
|
|
167
|
+
print(f"\033[32mCreated {item_type}:\033[0m {display_name}")
|
|
168
|
+
else:
|
|
169
|
+
print(f"Created {item_type}: {display_name}")
|
|
170
|
+
|
|
171
|
+
return filepath
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class MakeCommand:
|
|
175
|
+
"""Command dispatcher for code generation scaffolding."""
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def register(
|
|
179
|
+
subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
|
|
180
|
+
) -> None:
|
|
181
|
+
# 1. make:migration
|
|
182
|
+
parser_mig = subparsers.add_parser(
|
|
183
|
+
"make:migration",
|
|
184
|
+
help="Create a new migration file",
|
|
185
|
+
)
|
|
186
|
+
parser_mig.add_argument(
|
|
187
|
+
"name",
|
|
188
|
+
help="The name of the migration (e.g., create_users_table)",
|
|
189
|
+
)
|
|
190
|
+
parser_mig.add_argument(
|
|
191
|
+
"--path",
|
|
192
|
+
dest="path",
|
|
193
|
+
default=None,
|
|
194
|
+
help="The directory where the migration should be saved",
|
|
195
|
+
)
|
|
196
|
+
parser_mig.add_argument(
|
|
197
|
+
"--create",
|
|
198
|
+
dest="create",
|
|
199
|
+
default=None,
|
|
200
|
+
help="The table to be created",
|
|
201
|
+
)
|
|
202
|
+
parser_mig.add_argument(
|
|
203
|
+
"--table",
|
|
204
|
+
dest="table",
|
|
205
|
+
default=None,
|
|
206
|
+
help="The table to be altered",
|
|
207
|
+
)
|
|
208
|
+
parser_mig.add_argument(
|
|
209
|
+
"--force",
|
|
210
|
+
action="store_true",
|
|
211
|
+
help="Overwrite the migration if a file with the same name already exists",
|
|
212
|
+
)
|
|
213
|
+
parser_mig.set_defaults(
|
|
214
|
+
_handler=MakeCommand.handle_migration,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# 2. make:model
|
|
218
|
+
parser_model = subparsers.add_parser(
|
|
219
|
+
"make:model",
|
|
220
|
+
help="Create a new Model class",
|
|
221
|
+
)
|
|
222
|
+
parser_model.add_argument(
|
|
223
|
+
"name",
|
|
224
|
+
help="The name of the model (e.g., User)",
|
|
225
|
+
)
|
|
226
|
+
parser_model.add_argument(
|
|
227
|
+
"-m",
|
|
228
|
+
"--migration",
|
|
229
|
+
action="store_true",
|
|
230
|
+
help="Create a new migration file for the model",
|
|
231
|
+
)
|
|
232
|
+
parser_model.add_argument(
|
|
233
|
+
"--table",
|
|
234
|
+
dest="table",
|
|
235
|
+
default=None,
|
|
236
|
+
help="The table associated with the model",
|
|
237
|
+
)
|
|
238
|
+
parser_model.add_argument(
|
|
239
|
+
"--path",
|
|
240
|
+
dest="path",
|
|
241
|
+
default=None,
|
|
242
|
+
help="The directory where the model should be saved (defaults to app/models)",
|
|
243
|
+
)
|
|
244
|
+
parser_model.add_argument(
|
|
245
|
+
"--migration-path",
|
|
246
|
+
dest="migration_path",
|
|
247
|
+
default=None,
|
|
248
|
+
help="The directory where the generated migration should be saved (defaults to database/migrations)",
|
|
249
|
+
)
|
|
250
|
+
parser_model.add_argument(
|
|
251
|
+
"--force",
|
|
252
|
+
action="store_true",
|
|
253
|
+
help="Overwrite the model if it already exists",
|
|
254
|
+
)
|
|
255
|
+
parser_model.set_defaults(
|
|
256
|
+
_handler=MakeCommand.handle_model,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# 3. make:service
|
|
260
|
+
parser_service = subparsers.add_parser(
|
|
261
|
+
"make:service",
|
|
262
|
+
help="Create a new Service class",
|
|
263
|
+
)
|
|
264
|
+
parser_service.add_argument(
|
|
265
|
+
"name",
|
|
266
|
+
help="The name of the service (e.g., PaymentService)",
|
|
267
|
+
)
|
|
268
|
+
parser_service.add_argument(
|
|
269
|
+
"--path",
|
|
270
|
+
dest="path",
|
|
271
|
+
default=None,
|
|
272
|
+
help="The directory where the service should be saved (defaults to app/services)",
|
|
273
|
+
)
|
|
274
|
+
parser_service.add_argument(
|
|
275
|
+
"--force",
|
|
276
|
+
action="store_true",
|
|
277
|
+
help="Overwrite the service if it already exists",
|
|
278
|
+
)
|
|
279
|
+
parser_service.set_defaults(
|
|
280
|
+
_handler=MakeCommand.handle_service,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# 4. make:request
|
|
284
|
+
parser_request = subparsers.add_parser(
|
|
285
|
+
"make:request",
|
|
286
|
+
help="Create a new FormRequest validation class",
|
|
287
|
+
)
|
|
288
|
+
parser_request.add_argument(
|
|
289
|
+
"name",
|
|
290
|
+
help="The name of the request (e.g., CreateUserRequest)",
|
|
291
|
+
)
|
|
292
|
+
parser_request.add_argument(
|
|
293
|
+
"--path",
|
|
294
|
+
dest="path",
|
|
295
|
+
default=None,
|
|
296
|
+
help="The directory where the request should be saved (defaults to app/http/requests)",
|
|
297
|
+
)
|
|
298
|
+
parser_request.add_argument(
|
|
299
|
+
"--force",
|
|
300
|
+
action="store_true",
|
|
301
|
+
help="Overwrite the request if it already exists",
|
|
302
|
+
)
|
|
303
|
+
parser_request.set_defaults(
|
|
304
|
+
_handler=MakeCommand.handle_request,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# 5. make:resource
|
|
308
|
+
parser_resource = subparsers.add_parser(
|
|
309
|
+
"make:resource",
|
|
310
|
+
help="Create a new Resource transformer class",
|
|
311
|
+
)
|
|
312
|
+
parser_resource.add_argument(
|
|
313
|
+
"name",
|
|
314
|
+
help="The name of the resource (e.g., UserResource)",
|
|
315
|
+
)
|
|
316
|
+
parser_resource.add_argument(
|
|
317
|
+
"--path",
|
|
318
|
+
dest="path",
|
|
319
|
+
default=None,
|
|
320
|
+
help="The directory where the resource should be saved (defaults to app/http/resources)",
|
|
321
|
+
)
|
|
322
|
+
parser_resource.add_argument(
|
|
323
|
+
"--force",
|
|
324
|
+
action="store_true",
|
|
325
|
+
help="Overwrite the resource if it already exists",
|
|
326
|
+
)
|
|
327
|
+
parser_resource.set_defaults(
|
|
328
|
+
_handler=MakeCommand.handle_resource,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# 6. make:repository
|
|
332
|
+
parser_repo = subparsers.add_parser(
|
|
333
|
+
"make:repository",
|
|
334
|
+
help="Create a new Repository class",
|
|
335
|
+
)
|
|
336
|
+
parser_repo.add_argument(
|
|
337
|
+
"name",
|
|
338
|
+
help="The name of the repository (e.g., UserRepository)",
|
|
339
|
+
)
|
|
340
|
+
parser_repo.add_argument(
|
|
341
|
+
"--path",
|
|
342
|
+
dest="path",
|
|
343
|
+
default=None,
|
|
344
|
+
help="The directory where the repository should be saved (defaults to app/repositories)",
|
|
345
|
+
)
|
|
346
|
+
parser_repo.add_argument(
|
|
347
|
+
"--force",
|
|
348
|
+
action="store_true",
|
|
349
|
+
help="Overwrite the repository if it already exists",
|
|
350
|
+
)
|
|
351
|
+
parser_repo.set_defaults(
|
|
352
|
+
_handler=MakeCommand.handle_repository,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
# 7. make:schema
|
|
356
|
+
parser_schema = subparsers.add_parser(
|
|
357
|
+
"make:schema",
|
|
358
|
+
help="Create a new Schema class",
|
|
359
|
+
)
|
|
360
|
+
parser_schema.add_argument(
|
|
361
|
+
"name",
|
|
362
|
+
help="The name of the schema (e.g., OrderSchema)",
|
|
363
|
+
)
|
|
364
|
+
parser_schema.add_argument(
|
|
365
|
+
"--path",
|
|
366
|
+
dest="path",
|
|
367
|
+
default=None,
|
|
368
|
+
help="The directory where the schema should be saved (defaults to app/schemas)",
|
|
369
|
+
)
|
|
370
|
+
parser_schema.add_argument(
|
|
371
|
+
"--force",
|
|
372
|
+
action="store_true",
|
|
373
|
+
help="Overwrite the schema if it already exists",
|
|
374
|
+
)
|
|
375
|
+
parser_schema.set_defaults(
|
|
376
|
+
_handler=MakeCommand.handle_schema,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# 8. make:mail
|
|
380
|
+
parser_mail = subparsers.add_parser(
|
|
381
|
+
"make:mail",
|
|
382
|
+
help="Create a new Mailable class",
|
|
383
|
+
)
|
|
384
|
+
parser_mail.add_argument(
|
|
385
|
+
"name",
|
|
386
|
+
help="The name of the mailable (e.g., WelcomeEmail)",
|
|
387
|
+
)
|
|
388
|
+
parser_mail.add_argument(
|
|
389
|
+
"--path",
|
|
390
|
+
dest="path",
|
|
391
|
+
default=None,
|
|
392
|
+
help="The directory where the mailable should be saved (defaults to app/mail)",
|
|
393
|
+
)
|
|
394
|
+
parser_mail.add_argument(
|
|
395
|
+
"--force",
|
|
396
|
+
action="store_true",
|
|
397
|
+
help="Overwrite the mailable if it already exists",
|
|
398
|
+
)
|
|
399
|
+
parser_mail.set_defaults(
|
|
400
|
+
_handler=MakeCommand.handle_mail,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# 9. make:api-key
|
|
404
|
+
parser_api_key = subparsers.add_parser(
|
|
405
|
+
"make:api-key",
|
|
406
|
+
help="Generate a new prefix.secret API key and its peppered HMAC-SHA256 hash",
|
|
407
|
+
)
|
|
408
|
+
parser_api_key.add_argument(
|
|
409
|
+
"--prefix",
|
|
410
|
+
dest="prefix",
|
|
411
|
+
default="sk_live_",
|
|
412
|
+
help="Prefix for the API key (default: sk_live_)",
|
|
413
|
+
)
|
|
414
|
+
parser_api_key.add_argument(
|
|
415
|
+
"--pepper",
|
|
416
|
+
dest="pepper",
|
|
417
|
+
default=None,
|
|
418
|
+
help="Server pepper for HMAC-SHA256 hashing (defaults to AUTH_KEY_PEPPER config)",
|
|
419
|
+
)
|
|
420
|
+
parser_api_key.add_argument(
|
|
421
|
+
"-m",
|
|
422
|
+
"--migration",
|
|
423
|
+
action="store_true",
|
|
424
|
+
help="Also create the api_keys table migration file",
|
|
425
|
+
)
|
|
426
|
+
parser_api_key.add_argument(
|
|
427
|
+
"--path",
|
|
428
|
+
dest="path",
|
|
429
|
+
default=None,
|
|
430
|
+
help="Migration target directory when --migration is specified",
|
|
431
|
+
)
|
|
432
|
+
parser_api_key.add_argument(
|
|
433
|
+
"--force",
|
|
434
|
+
action="store_true",
|
|
435
|
+
help="Overwrite the generated migration if it already exists",
|
|
436
|
+
)
|
|
437
|
+
parser_api_key.set_defaults(
|
|
438
|
+
_handler=MakeCommand.handle_api_key,
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
# 10. make:controller
|
|
442
|
+
parser_controller = subparsers.add_parser(
|
|
443
|
+
"make:controller",
|
|
444
|
+
help="Create a new Controller class",
|
|
445
|
+
)
|
|
446
|
+
parser_controller.add_argument(
|
|
447
|
+
"name",
|
|
448
|
+
help="The name of the controller (e.g., UserController)",
|
|
449
|
+
)
|
|
450
|
+
parser_controller.add_argument(
|
|
451
|
+
"--path",
|
|
452
|
+
dest="path",
|
|
453
|
+
default=None,
|
|
454
|
+
help="The directory where the controller should be saved (defaults to app/http/controllers)",
|
|
455
|
+
)
|
|
456
|
+
parser_controller.add_argument(
|
|
457
|
+
"--force",
|
|
458
|
+
action="store_true",
|
|
459
|
+
help="Overwrite the controller if it already exists",
|
|
460
|
+
)
|
|
461
|
+
parser_controller.set_defaults(
|
|
462
|
+
_handler=MakeCommand.handle_controller,
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
# 11. make:middleware
|
|
466
|
+
parser_middleware = subparsers.add_parser(
|
|
467
|
+
"make:middleware",
|
|
468
|
+
help="Create a new ASGI Middleware class",
|
|
469
|
+
)
|
|
470
|
+
parser_middleware.add_argument(
|
|
471
|
+
"name",
|
|
472
|
+
help="The name of the middleware (e.g., AuthMiddleware)",
|
|
473
|
+
)
|
|
474
|
+
parser_middleware.add_argument(
|
|
475
|
+
"--path",
|
|
476
|
+
dest="path",
|
|
477
|
+
default=None,
|
|
478
|
+
help="The directory where the middleware should be saved (defaults to app/http/middleware)",
|
|
479
|
+
)
|
|
480
|
+
parser_middleware.add_argument(
|
|
481
|
+
"--force",
|
|
482
|
+
action="store_true",
|
|
483
|
+
help="Overwrite the middleware if it already exists",
|
|
484
|
+
)
|
|
485
|
+
parser_middleware.set_defaults(
|
|
486
|
+
_handler=MakeCommand.handle_middleware,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
# 12. make:job
|
|
490
|
+
parser_job = subparsers.add_parser(
|
|
491
|
+
"make:job",
|
|
492
|
+
help="Create a new queued background Job class",
|
|
493
|
+
)
|
|
494
|
+
parser_job.add_argument(
|
|
495
|
+
"name",
|
|
496
|
+
help="The name of the job (e.g., SendNotificationJob)",
|
|
497
|
+
)
|
|
498
|
+
parser_job.add_argument(
|
|
499
|
+
"--path",
|
|
500
|
+
dest="path",
|
|
501
|
+
default=None,
|
|
502
|
+
help="The directory where the job should be saved (defaults to app/jobs)",
|
|
503
|
+
)
|
|
504
|
+
parser_job.add_argument(
|
|
505
|
+
"--force",
|
|
506
|
+
action="store_true",
|
|
507
|
+
help="Overwrite the job if it already exists",
|
|
508
|
+
)
|
|
509
|
+
parser_job.set_defaults(
|
|
510
|
+
_handler=MakeCommand.handle_job,
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
# 13. make:event
|
|
514
|
+
parser_event = subparsers.add_parser(
|
|
515
|
+
"make:event",
|
|
516
|
+
help="Create a new domain Event dataclass",
|
|
517
|
+
)
|
|
518
|
+
parser_event.add_argument(
|
|
519
|
+
"name",
|
|
520
|
+
help="The name of the event (e.g., UserRegistered)",
|
|
521
|
+
)
|
|
522
|
+
parser_event.add_argument(
|
|
523
|
+
"--path",
|
|
524
|
+
dest="path",
|
|
525
|
+
default=None,
|
|
526
|
+
help="The directory where the event should be saved (defaults to app/events)",
|
|
527
|
+
)
|
|
528
|
+
parser_event.add_argument(
|
|
529
|
+
"--force",
|
|
530
|
+
action="store_true",
|
|
531
|
+
help="Overwrite the event if it already exists",
|
|
532
|
+
)
|
|
533
|
+
parser_event.set_defaults(
|
|
534
|
+
_handler=MakeCommand.handle_event,
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
# 14. make:listener
|
|
538
|
+
parser_listener = subparsers.add_parser(
|
|
539
|
+
"make:listener",
|
|
540
|
+
help="Create a new event Listener class",
|
|
541
|
+
)
|
|
542
|
+
parser_listener.add_argument(
|
|
543
|
+
"name",
|
|
544
|
+
help="The name of the listener (e.g., SendWelcomeNotification)",
|
|
545
|
+
)
|
|
546
|
+
parser_listener.add_argument(
|
|
547
|
+
"--path",
|
|
548
|
+
dest="path",
|
|
549
|
+
default=None,
|
|
550
|
+
help="The directory where the listener should be saved (defaults to app/listeners)",
|
|
551
|
+
)
|
|
552
|
+
parser_listener.add_argument(
|
|
553
|
+
"--force",
|
|
554
|
+
action="store_true",
|
|
555
|
+
help="Overwrite the listener if it already exists",
|
|
556
|
+
)
|
|
557
|
+
parser_listener.set_defaults(
|
|
558
|
+
_handler=MakeCommand.handle_listener,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
# 15. make:provider
|
|
562
|
+
parser_provider = subparsers.add_parser(
|
|
563
|
+
"make:provider",
|
|
564
|
+
help="Create a new ServiceProvider class",
|
|
565
|
+
)
|
|
566
|
+
parser_provider.add_argument(
|
|
567
|
+
"name",
|
|
568
|
+
help="The name of the service provider (e.g., AppServiceProvider)",
|
|
569
|
+
)
|
|
570
|
+
parser_provider.add_argument(
|
|
571
|
+
"--path",
|
|
572
|
+
dest="path",
|
|
573
|
+
default=None,
|
|
574
|
+
help="The directory where the provider should be saved (defaults to app/providers)",
|
|
575
|
+
)
|
|
576
|
+
parser_provider.add_argument(
|
|
577
|
+
"--force",
|
|
578
|
+
action="store_true",
|
|
579
|
+
help="Overwrite the provider if it already exists",
|
|
580
|
+
)
|
|
581
|
+
parser_provider.set_defaults(
|
|
582
|
+
_handler=MakeCommand.handle_provider,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
# 16. make:seeder
|
|
586
|
+
parser_seeder = subparsers.add_parser(
|
|
587
|
+
"make:seeder",
|
|
588
|
+
help="Create a new database Seeder class",
|
|
589
|
+
)
|
|
590
|
+
parser_seeder.add_argument(
|
|
591
|
+
"name",
|
|
592
|
+
help="The name of the seeder (e.g., UserSeeder)",
|
|
593
|
+
)
|
|
594
|
+
parser_seeder.add_argument(
|
|
595
|
+
"--path",
|
|
596
|
+
dest="path",
|
|
597
|
+
default=None,
|
|
598
|
+
help="The directory where the seeder should be saved (defaults to database/seeders)",
|
|
599
|
+
)
|
|
600
|
+
parser_seeder.add_argument(
|
|
601
|
+
"--force",
|
|
602
|
+
action="store_true",
|
|
603
|
+
help="Overwrite the seeder if it already exists",
|
|
604
|
+
)
|
|
605
|
+
parser_seeder.set_defaults(
|
|
606
|
+
_handler=MakeCommand.handle_seeder,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
@staticmethod
|
|
610
|
+
def handle_migration(args: argparse.Namespace) -> None:
|
|
611
|
+
"""Handle make:migration command execution."""
|
|
612
|
+
raw_name: str = args.name.strip()
|
|
613
|
+
if not raw_name or "/" in raw_name or "\\" in raw_name or ".." in raw_name:
|
|
614
|
+
raise SystemExit(f"Invalid migration name: {raw_name!r}")
|
|
615
|
+
|
|
616
|
+
snake_name = re.sub(r"[^a-zA-Z0-9_]+", "_", raw_name.lower()).strip("_")
|
|
617
|
+
if not snake_name or not _SEGMENT_PATTERN.fullmatch(snake_name):
|
|
618
|
+
raise SystemExit(f"Invalid migration name: {raw_name!r}")
|
|
619
|
+
|
|
620
|
+
class_name = _pascal_case(snake_name)
|
|
621
|
+
if not class_name or not _SEGMENT_PATTERN.fullmatch(class_name):
|
|
622
|
+
raise SystemExit(f"Invalid migration class name: {class_name!r}")
|
|
623
|
+
|
|
624
|
+
raw_table = getattr(args, "create", None) or getattr(args, "table", None)
|
|
625
|
+
is_create = bool(getattr(args, "create", None))
|
|
626
|
+
if raw_table is not None:
|
|
627
|
+
try:
|
|
628
|
+
table_name: str | None = validate_identifier(
|
|
629
|
+
str(raw_table), param_name="table"
|
|
630
|
+
)
|
|
631
|
+
except ValueError as exc:
|
|
632
|
+
raise SystemExit(f"Invalid table identifier: {exc}") from exc
|
|
633
|
+
else:
|
|
634
|
+
guessed_table, is_create = _guess_table(snake_name)
|
|
635
|
+
if guessed_table is not None:
|
|
636
|
+
try:
|
|
637
|
+
table_name = validate_identifier(
|
|
638
|
+
guessed_table, param_name="table"
|
|
639
|
+
)
|
|
640
|
+
except ValueError as exc:
|
|
641
|
+
raise SystemExit(f"Invalid table identifier: {exc}") from exc
|
|
642
|
+
else:
|
|
643
|
+
table_name = None
|
|
644
|
+
|
|
645
|
+
target_dir = (
|
|
646
|
+
Path(args.path) if args.path else Path.cwd() / "database" / "migrations"
|
|
647
|
+
)
|
|
648
|
+
resolved_base = target_dir.resolve()
|
|
649
|
+
timestamp = datetime.now(tz=UTC).strftime("%Y_%m_%d_%H%M%S")
|
|
650
|
+
filename = f"{timestamp}_{snake_name}.py"
|
|
651
|
+
filepath = target_dir / filename
|
|
652
|
+
|
|
653
|
+
if not filepath.resolve().is_relative_to(resolved_base):
|
|
654
|
+
raise SystemExit(
|
|
655
|
+
f"Invalid migration path {raw_name!r}: target escapes base directory '{resolved_base}'."
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
target_dir.mkdir(parents=True, exist_ok=True)
|
|
659
|
+
if not filepath.resolve().is_relative_to(resolved_base) or filepath.is_symlink():
|
|
660
|
+
raise SystemExit(
|
|
661
|
+
f"Invalid migration path {raw_name!r}: target escapes base directory '{resolved_base}'."
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
if filepath.exists() and not getattr(args, "force", False):
|
|
665
|
+
raise SystemExit(
|
|
666
|
+
f"Migration already exists: {filepath}. Use --force to overwrite."
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
stub_override = getattr(args, "stub", None)
|
|
670
|
+
if stub_override:
|
|
671
|
+
content = (
|
|
672
|
+
get_stub(stub_override)
|
|
673
|
+
.replace("{class_name}", class_name)
|
|
674
|
+
.replace("{table_name}", table_name or "api_keys")
|
|
675
|
+
)
|
|
676
|
+
elif table_name is None:
|
|
677
|
+
content = get_stub("migration.blank").replace("{class_name}", class_name)
|
|
678
|
+
elif is_create and table_name == "api_keys":
|
|
679
|
+
content = (
|
|
680
|
+
get_stub("migration.api_keys")
|
|
681
|
+
.replace("{class_name}", class_name)
|
|
682
|
+
.replace("{table_name}", table_name)
|
|
683
|
+
)
|
|
684
|
+
elif is_create:
|
|
685
|
+
content = (
|
|
686
|
+
get_stub("migration.create")
|
|
687
|
+
.replace("{class_name}", class_name)
|
|
688
|
+
.replace("{table_name}", table_name)
|
|
689
|
+
)
|
|
690
|
+
else:
|
|
691
|
+
content = (
|
|
692
|
+
get_stub("migration.alter")
|
|
693
|
+
.replace("{class_name}", class_name)
|
|
694
|
+
.replace("{table_name}", table_name)
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
filepath.write_text(content, encoding="utf-8")
|
|
698
|
+
if sys.stdout.isatty() and "NO_COLOR" not in os.environ:
|
|
699
|
+
print(f"\033[32mCreated Migration:\033[0m {filename}")
|
|
700
|
+
else:
|
|
701
|
+
print(f"Created Migration: {filename}")
|
|
702
|
+
|
|
703
|
+
@staticmethod
|
|
704
|
+
def handle_model(args: argparse.Namespace) -> None:
|
|
705
|
+
"""Handle make:model command execution."""
|
|
706
|
+
raw_name: str = args.name.strip()
|
|
707
|
+
_, snake_name, _ = _validate_and_split_name(
|
|
708
|
+
raw_name,
|
|
709
|
+
item_type="Model",
|
|
710
|
+
allow_nested=True,
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
raw_table = getattr(args, "table", None)
|
|
714
|
+
if raw_table is not None:
|
|
715
|
+
try:
|
|
716
|
+
table_name = validate_identifier(str(raw_table), param_name="table")
|
|
717
|
+
except ValueError as exc:
|
|
718
|
+
raise SystemExit(f"Invalid table identifier: {exc}") from exc
|
|
719
|
+
else:
|
|
720
|
+
try:
|
|
721
|
+
table_name = validate_identifier(
|
|
722
|
+
pluralize(snake_name), param_name="table"
|
|
723
|
+
)
|
|
724
|
+
except ValueError as exc:
|
|
725
|
+
raise SystemExit(f"Invalid table identifier: {exc}") from exc
|
|
726
|
+
|
|
727
|
+
_scaffold_file(
|
|
728
|
+
raw_name=raw_name,
|
|
729
|
+
default_dir_rel="app/models",
|
|
730
|
+
stub_name="model",
|
|
731
|
+
override_path=getattr(args, "path", None),
|
|
732
|
+
force=bool(getattr(args, "force", False)),
|
|
733
|
+
extra_replacements={"table_name": table_name},
|
|
734
|
+
item_type="Model",
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
if getattr(args, "migration", False):
|
|
738
|
+
# If -m is added, create migration file for that model
|
|
739
|
+
mig_name = f"create_{table_name}_table"
|
|
740
|
+
mig_args = argparse.Namespace(
|
|
741
|
+
name=mig_name,
|
|
742
|
+
path=getattr(args, "migration_path", None),
|
|
743
|
+
create=table_name,
|
|
744
|
+
table=None,
|
|
745
|
+
force=bool(getattr(args, "force", False)),
|
|
746
|
+
)
|
|
747
|
+
MakeCommand.handle_migration(mig_args)
|
|
748
|
+
|
|
749
|
+
@staticmethod
|
|
750
|
+
def handle_api_key(args: argparse.Namespace) -> None:
|
|
751
|
+
"""Handle make:api-key command execution (P2-A2 requirement 5)."""
|
|
752
|
+
prefix_arg: str = getattr(args, "prefix", "sk_live_") or "sk_live_"
|
|
753
|
+
pepper_arg: str | None = getattr(args, "pepper", None)
|
|
754
|
+
|
|
755
|
+
try:
|
|
756
|
+
active_pepper: str | bytes = (
|
|
757
|
+
pepper_arg if pepper_arg is not None else get_server_pepper()
|
|
758
|
+
)
|
|
759
|
+
except ConfigurationError as exc:
|
|
760
|
+
raise SystemExit(str(exc)) from exc
|
|
761
|
+
|
|
762
|
+
full_key, key_prefix, secret = generate_api_key(prefix=prefix_arg)
|
|
763
|
+
key_hash = hash_api_key(secret, pepper=active_pepper)
|
|
764
|
+
|
|
765
|
+
print("Generated API Key (save this secret now; it will not be shown again):")
|
|
766
|
+
print(f" API Key: {full_key}")
|
|
767
|
+
print(f" key_prefix: {key_prefix}")
|
|
768
|
+
print(f" key_hash: {key_hash}")
|
|
769
|
+
|
|
770
|
+
if getattr(args, "migration", False):
|
|
771
|
+
mig_args = argparse.Namespace(
|
|
772
|
+
name="create_api_keys_table",
|
|
773
|
+
path=getattr(args, "path", None),
|
|
774
|
+
create="api_keys",
|
|
775
|
+
table=None,
|
|
776
|
+
stub="migration.api_keys",
|
|
777
|
+
force=bool(getattr(args, "force", False)),
|
|
778
|
+
)
|
|
779
|
+
MakeCommand.handle_migration(mig_args)
|
|
780
|
+
|
|
781
|
+
@staticmethod
|
|
782
|
+
def handle_service(args: argparse.Namespace) -> None:
|
|
783
|
+
"""Handle make:service command execution."""
|
|
784
|
+
_scaffold_file(
|
|
785
|
+
raw_name=args.name,
|
|
786
|
+
default_dir_rel="app/services",
|
|
787
|
+
stub_name="service",
|
|
788
|
+
override_path=getattr(args, "path", None),
|
|
789
|
+
force=bool(getattr(args, "force", False)),
|
|
790
|
+
item_type="Service",
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
@staticmethod
|
|
794
|
+
def handle_request(args: argparse.Namespace) -> None:
|
|
795
|
+
"""Handle make:request command execution."""
|
|
796
|
+
_scaffold_file(
|
|
797
|
+
raw_name=args.name,
|
|
798
|
+
default_dir_rel="app/http/requests",
|
|
799
|
+
stub_name="request",
|
|
800
|
+
override_path=getattr(args, "path", None),
|
|
801
|
+
force=bool(getattr(args, "force", False)),
|
|
802
|
+
item_type="Request",
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
@staticmethod
|
|
806
|
+
def handle_resource(args: argparse.Namespace) -> None:
|
|
807
|
+
"""Handle make:resource command execution."""
|
|
808
|
+
_scaffold_file(
|
|
809
|
+
raw_name=args.name,
|
|
810
|
+
default_dir_rel="app/http/resources",
|
|
811
|
+
stub_name="resource",
|
|
812
|
+
override_path=getattr(args, "path", None),
|
|
813
|
+
force=bool(getattr(args, "force", False)),
|
|
814
|
+
item_type="Resource",
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
@staticmethod
|
|
818
|
+
def handle_repository(args: argparse.Namespace) -> None:
|
|
819
|
+
"""Handle make:repository command execution."""
|
|
820
|
+
_scaffold_file(
|
|
821
|
+
raw_name=args.name,
|
|
822
|
+
default_dir_rel="app/repositories",
|
|
823
|
+
stub_name="repository",
|
|
824
|
+
override_path=getattr(args, "path", None),
|
|
825
|
+
force=bool(getattr(args, "force", False)),
|
|
826
|
+
item_type="Repository",
|
|
827
|
+
)
|
|
828
|
+
|
|
829
|
+
@staticmethod
|
|
830
|
+
def handle_schema(args: argparse.Namespace) -> None:
|
|
831
|
+
"""Handle make:schema command execution."""
|
|
832
|
+
_scaffold_file(
|
|
833
|
+
raw_name=args.name,
|
|
834
|
+
default_dir_rel="app/schemas",
|
|
835
|
+
stub_name="schema",
|
|
836
|
+
override_path=getattr(args, "path", None),
|
|
837
|
+
force=bool(getattr(args, "force", False)),
|
|
838
|
+
item_type="Schema",
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
@staticmethod
|
|
842
|
+
def handle_mail(args: argparse.Namespace) -> None:
|
|
843
|
+
"""Handle make:mail command execution."""
|
|
844
|
+
_scaffold_file(
|
|
845
|
+
raw_name=args.name,
|
|
846
|
+
default_dir_rel="app/mail",
|
|
847
|
+
stub_name="mail",
|
|
848
|
+
override_path=getattr(args, "path", None),
|
|
849
|
+
force=bool(getattr(args, "force", False)),
|
|
850
|
+
item_type="Mailable",
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
@staticmethod
|
|
854
|
+
def handle_controller(args: argparse.Namespace) -> None:
|
|
855
|
+
"""Handle make:controller command execution."""
|
|
856
|
+
_scaffold_file(
|
|
857
|
+
raw_name=args.name,
|
|
858
|
+
default_dir_rel="app/http/controllers",
|
|
859
|
+
stub_name="controller",
|
|
860
|
+
override_path=getattr(args, "path", None),
|
|
861
|
+
force=bool(getattr(args, "force", False)),
|
|
862
|
+
item_type="Controller",
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
@staticmethod
|
|
866
|
+
def handle_middleware(args: argparse.Namespace) -> None:
|
|
867
|
+
"""Handle make:middleware command execution."""
|
|
868
|
+
_scaffold_file(
|
|
869
|
+
raw_name=args.name,
|
|
870
|
+
default_dir_rel="app/http/middleware",
|
|
871
|
+
stub_name="middleware",
|
|
872
|
+
override_path=getattr(args, "path", None),
|
|
873
|
+
force=bool(getattr(args, "force", False)),
|
|
874
|
+
item_type="Middleware",
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
@staticmethod
|
|
878
|
+
def handle_job(args: argparse.Namespace) -> None:
|
|
879
|
+
"""Handle make:job command execution."""
|
|
880
|
+
_scaffold_file(
|
|
881
|
+
raw_name=args.name,
|
|
882
|
+
default_dir_rel="app/jobs",
|
|
883
|
+
stub_name="job",
|
|
884
|
+
override_path=getattr(args, "path", None),
|
|
885
|
+
force=bool(getattr(args, "force", False)),
|
|
886
|
+
item_type="Job",
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
@staticmethod
|
|
890
|
+
def handle_event(args: argparse.Namespace) -> None:
|
|
891
|
+
"""Handle make:event command execution."""
|
|
892
|
+
_scaffold_file(
|
|
893
|
+
raw_name=args.name,
|
|
894
|
+
default_dir_rel="app/events",
|
|
895
|
+
stub_name="event",
|
|
896
|
+
override_path=getattr(args, "path", None),
|
|
897
|
+
force=bool(getattr(args, "force", False)),
|
|
898
|
+
item_type="Event",
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
@staticmethod
|
|
902
|
+
def handle_listener(args: argparse.Namespace) -> None:
|
|
903
|
+
"""Handle make:listener command execution."""
|
|
904
|
+
_scaffold_file(
|
|
905
|
+
raw_name=args.name,
|
|
906
|
+
default_dir_rel="app/listeners",
|
|
907
|
+
stub_name="listener",
|
|
908
|
+
override_path=getattr(args, "path", None),
|
|
909
|
+
force=bool(getattr(args, "force", False)),
|
|
910
|
+
item_type="Listener",
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
@staticmethod
|
|
914
|
+
def handle_provider(args: argparse.Namespace) -> None:
|
|
915
|
+
"""Handle make:provider command execution."""
|
|
916
|
+
_scaffold_file(
|
|
917
|
+
raw_name=args.name,
|
|
918
|
+
default_dir_rel="app/providers",
|
|
919
|
+
stub_name="provider",
|
|
920
|
+
override_path=getattr(args, "path", None),
|
|
921
|
+
force=bool(getattr(args, "force", False)),
|
|
922
|
+
item_type="Provider",
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
@staticmethod
|
|
926
|
+
def handle_seeder(args: argparse.Namespace) -> None:
|
|
927
|
+
"""Handle make:seeder command execution."""
|
|
928
|
+
_scaffold_file(
|
|
929
|
+
raw_name=args.name,
|
|
930
|
+
default_dir_rel="database/seeders",
|
|
931
|
+
stub_name="seeder",
|
|
932
|
+
override_path=getattr(args, "path", None),
|
|
933
|
+
force=bool(getattr(args, "force", False)),
|
|
934
|
+
item_type="Seeder",
|
|
935
|
+
)
|