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.
Files changed (211) hide show
  1. swiftpy/.gitignore +0 -0
  2. swiftpy/__init__.py +572 -0
  3. swiftpy/admin/__init__.py +50 -0
  4. swiftpy/admin/auth.py +522 -0
  5. swiftpy/admin/registry.py +567 -0
  6. swiftpy/admin/views.py +1583 -0
  7. swiftpy/ai/__init__.py +199 -0
  8. swiftpy/ai/_deterministic.py +40 -0
  9. swiftpy/ai/_fake.py +604 -0
  10. swiftpy/ai/agents/__init__.py +30 -0
  11. swiftpy/ai/agents/base_agent.py +506 -0
  12. swiftpy/ai/agents/memory.py +864 -0
  13. swiftpy/ai/client.py +1011 -0
  14. swiftpy/ai/conversation.py +171 -0
  15. swiftpy/ai/embeddings.py +321 -0
  16. swiftpy/ai/exceptions.py +75 -0
  17. swiftpy/ai/history_strategies.py +157 -0
  18. swiftpy/ai/middleware.py +918 -0
  19. swiftpy/ai/observability/__init__.py +28 -0
  20. swiftpy/ai/observability/drivers.py +763 -0
  21. swiftpy/ai/observability/observer.py +364 -0
  22. swiftpy/ai/pool.py +102 -0
  23. swiftpy/ai/pricing.py +101 -0
  24. swiftpy/ai/prompts/__init__.py +6 -0
  25. swiftpy/ai/prompts/loader.py +343 -0
  26. swiftpy/ai/providers/__init__.py +22 -0
  27. swiftpy/ai/providers/anthropic_provider.py +353 -0
  28. swiftpy/ai/providers/base.py +273 -0
  29. swiftpy/ai/providers/openai_provider.py +275 -0
  30. swiftpy/ai/rag/__init__.py +53 -0
  31. swiftpy/ai/rag/context_formatter.py +263 -0
  32. swiftpy/ai/rag/pipeline_builder.py +544 -0
  33. swiftpy/ai/rag/reranker.py +484 -0
  34. swiftpy/ai/rag/result.py +78 -0
  35. swiftpy/ai/rag/retriever.py +315 -0
  36. swiftpy/ai/redis_store.py +227 -0
  37. swiftpy/ai/streaming.py +115 -0
  38. swiftpy/ai/tools/__init__.py +22 -0
  39. swiftpy/ai/tools/base_tool.py +489 -0
  40. swiftpy/ai/tools/first_party.py +384 -0
  41. swiftpy/ai/tools/tool_loop.py +417 -0
  42. swiftpy/ai/vectorstore/__init__.py +46 -0
  43. swiftpy/ai/vectorstore/base.py +542 -0
  44. swiftpy/ai/vectorstore/chroma_adapter.py +435 -0
  45. swiftpy/ai/vectorstore/in_memory_adapter.py +321 -0
  46. swiftpy/ai/vectorstore/lancedb_adapter.py +370 -0
  47. swiftpy/ai/vectorstore/pgvector_adapter.py +496 -0
  48. swiftpy/ai/vectorstore/pinecone_adapter.py +445 -0
  49. swiftpy/ai/vectorstore/qdrant_adapter.py +418 -0
  50. swiftpy/auth/__init__.py +50 -0
  51. swiftpy/auth/api_key_guard.py +544 -0
  52. swiftpy/auth/auth_manager.py +259 -0
  53. swiftpy/auth/exceptions.py +71 -0
  54. swiftpy/auth/jwt_guard.py +230 -0
  55. swiftpy/auth/jwt_issuer.py +658 -0
  56. swiftpy/auth/token_blacklist.py +300 -0
  57. swiftpy/cache/__init__.py +14 -0
  58. swiftpy/cache/cache.py +251 -0
  59. swiftpy/cache/driver.py +216 -0
  60. swiftpy/cache/drivers/memory.py +199 -0
  61. swiftpy/cache/drivers/redis.py +421 -0
  62. swiftpy/console/__init__.py +0 -0
  63. swiftpy/console/cli.py +68 -0
  64. swiftpy/console/commands/__init__.py +0 -0
  65. swiftpy/console/commands/cache.py +164 -0
  66. swiftpy/console/commands/make.py +935 -0
  67. swiftpy/console/commands/migrate.py +280 -0
  68. swiftpy/console/commands/new.py +1806 -0
  69. swiftpy/console/commands/queue.py +306 -0
  70. swiftpy/console/commands/seed.py +252 -0
  71. swiftpy/console/commands/start.py +171 -0
  72. swiftpy/console/commands/storage.py +104 -0
  73. swiftpy/console/loader.py +21 -0
  74. swiftpy/console/stubs/__init__.py +26 -0
  75. swiftpy/console/stubs/controller.stub +14 -0
  76. swiftpy/console/stubs/event.stub +14 -0
  77. swiftpy/console/stubs/job.stub +22 -0
  78. swiftpy/console/stubs/listener.stub +14 -0
  79. swiftpy/console/stubs/mail.stub +20 -0
  80. swiftpy/console/stubs/middleware.stub +16 -0
  81. swiftpy/console/stubs/migration.alter.stub +17 -0
  82. swiftpy/console/stubs/migration.api_keys.stub +22 -0
  83. swiftpy/console/stubs/migration.blank.stub +11 -0
  84. swiftpy/console/stubs/migration.create.stub +15 -0
  85. swiftpy/console/stubs/migration.stub +11 -0
  86. swiftpy/console/stubs/migration.update.stub +17 -0
  87. swiftpy/console/stubs/model.stub +17 -0
  88. swiftpy/console/stubs/provider.stub +18 -0
  89. swiftpy/console/stubs/repository.stub +10 -0
  90. swiftpy/console/stubs/request.stub +16 -0
  91. swiftpy/console/stubs/resource.stub +21 -0
  92. swiftpy/console/stubs/schema.stub +12 -0
  93. swiftpy/console/stubs/seeder.stub +14 -0
  94. swiftpy/console/stubs/service.stub +10 -0
  95. swiftpy/core/__init__.py +0 -0
  96. swiftpy/core/application.py +1306 -0
  97. swiftpy/core/bootstrap.py +30 -0
  98. swiftpy/core/config.py +432 -0
  99. swiftpy/core/container.py +596 -0
  100. swiftpy/core/container_exceptions.py +18 -0
  101. swiftpy/core/context.py +148 -0
  102. swiftpy/core/exceptions.py +352 -0
  103. swiftpy/core/providers/base.py +35 -0
  104. swiftpy/database/__init__.py +41 -0
  105. swiftpy/database/exceptions.py +42 -0
  106. swiftpy/database/identifiers.py +105 -0
  107. swiftpy/database/migrations/__init__.py +36 -0
  108. swiftpy/database/migrations/blueprint.py +431 -0
  109. swiftpy/database/migrations/lock.py +104 -0
  110. swiftpy/database/migrations/migration.py +22 -0
  111. swiftpy/database/migrations/migrator.py +507 -0
  112. swiftpy/database/migrations/schema.py +435 -0
  113. swiftpy/database/orm/__init__.py +33 -0
  114. swiftpy/database/orm/fields.py +92 -0
  115. swiftpy/database/orm/model.py +2034 -0
  116. swiftpy/database/orm/registry.py +33 -0
  117. swiftpy/database/orm/relationships.py +836 -0
  118. swiftpy/database/pagination/__init__.py +16 -0
  119. swiftpy/database/pagination/contracts.py +60 -0
  120. swiftpy/database/pagination/paginator.py +112 -0
  121. swiftpy/database/pagination/simple_paginator.py +97 -0
  122. swiftpy/database/pool.py +218 -0
  123. swiftpy/database/query_builder.py +1803 -0
  124. swiftpy/database/seeder.py +39 -0
  125. swiftpy/database/transaction.py +276 -0
  126. swiftpy/events/__init__.py +10 -0
  127. swiftpy/events/dispatcher.py +308 -0
  128. swiftpy/events/redis_bus.py +552 -0
  129. swiftpy/filesystem/__init__.py +227 -0
  130. swiftpy/filesystem/disk.py +378 -0
  131. swiftpy/filesystem/drivers/__init__.py +11 -0
  132. swiftpy/filesystem/drivers/base.py +156 -0
  133. swiftpy/filesystem/drivers/local.py +559 -0
  134. swiftpy/filesystem/drivers/s3.py +655 -0
  135. swiftpy/filesystem/events.py +37 -0
  136. swiftpy/filesystem/exceptions.py +127 -0
  137. swiftpy/filesystem/manager.py +138 -0
  138. swiftpy/filesystem/metadata.py +22 -0
  139. swiftpy/filesystem/paths.py +99 -0
  140. swiftpy/filesystem/service_provider.py +17 -0
  141. swiftpy/filesystem/uploads/__init__.py +13 -0
  142. swiftpy/filesystem/uploads/handlers.py +362 -0
  143. swiftpy/filesystem/uploads/uploaded_file.py +222 -0
  144. swiftpy/filesystem/uploads/validators.py +186 -0
  145. swiftpy/filesystem/visibility.py +22 -0
  146. swiftpy/helpers/__init__.py +42 -0
  147. swiftpy/helpers/helper.py +180 -0
  148. swiftpy/http/exceptions.py +41 -0
  149. swiftpy/http/middleware.py +307 -0
  150. swiftpy/http/request.py +375 -0
  151. swiftpy/http/response.py +442 -0
  152. swiftpy/http/router.py +1050 -0
  153. swiftpy/mail/__init__.py +58 -0
  154. swiftpy/mail/attachments.py +255 -0
  155. swiftpy/mail/config.py +71 -0
  156. swiftpy/mail/envelope.py +183 -0
  157. swiftpy/mail/exceptions.py +90 -0
  158. swiftpy/mail/facade.py +312 -0
  159. swiftpy/mail/mailable.py +53 -0
  160. swiftpy/mail/manager.py +240 -0
  161. swiftpy/mail/message.py +186 -0
  162. swiftpy/mail/models.py +19 -0
  163. swiftpy/mail/queue/__init__.py +5 -0
  164. swiftpy/mail/queue/job.py +279 -0
  165. swiftpy/mail/service_provider.py +19 -0
  166. swiftpy/mail/templates/__init__.py +5 -0
  167. swiftpy/mail/templates/renderer.py +173 -0
  168. swiftpy/mail/transports/__init__.py +21 -0
  169. swiftpy/mail/transports/api/__init__.py +13 -0
  170. swiftpy/mail/transports/api/base.py +194 -0
  171. swiftpy/mail/transports/api/mailgun.py +158 -0
  172. swiftpy/mail/transports/api/resend.py +156 -0
  173. swiftpy/mail/transports/api/sendgrid.py +172 -0
  174. swiftpy/mail/transports/base.py +24 -0
  175. swiftpy/mail/transports/console.py +91 -0
  176. swiftpy/mail/transports/sendmail.py +128 -0
  177. swiftpy/mail/transports/smtp.py +466 -0
  178. swiftpy/queue/__init__.py +13 -0
  179. swiftpy/queue/dispatcher.py +813 -0
  180. swiftpy/queue/job.py +541 -0
  181. swiftpy/queue/worker.py +556 -0
  182. swiftpy/support/__init__.py +12 -0
  183. swiftpy/support/keys.py +108 -0
  184. swiftpy/support/redis_config.py +223 -0
  185. swiftpy/support/strings.py +114 -0
  186. swiftpy/testing/__init__.py +20 -0
  187. swiftpy/testing/fakes/__init__.py +11 -0
  188. swiftpy/testing/fakes/ai_fake.py +21 -0
  189. swiftpy/testing/fakes/vectorstore_fake.py +194 -0
  190. swiftpy/testing/fixtures.py +124 -0
  191. swiftpy/testing/http_client.py +730 -0
  192. swiftpy/testing/test_case.py +126 -0
  193. swiftpy/validation/__init__.py +97 -0
  194. swiftpy/validation/exceptions.py +26 -0
  195. swiftpy/validation/form_request.py +302 -0
  196. swiftpy/validation/protocols.py +33 -0
  197. swiftpy/validation/rules/__init__.py +89 -0
  198. swiftpy/validation/rules/builtin.py +391 -0
  199. swiftpy/validation/rules/exists.py +86 -0
  200. swiftpy/validation/rules/registry.py +290 -0
  201. swiftpy/validation/rules/required.py +55 -0
  202. swiftpy/validation/rules/unique.py +138 -0
  203. swiftpy/validation/validator.py +612 -0
  204. swiftpy/websockets/__init__.py +19 -0
  205. swiftpy/websockets/connection.py +317 -0
  206. swiftpy/websockets/manager.py +271 -0
  207. swiftpy_framework-0.1.0.dist-info/METADATA +603 -0
  208. swiftpy_framework-0.1.0.dist-info/RECORD +211 -0
  209. swiftpy_framework-0.1.0.dist-info/WHEEL +4 -0
  210. swiftpy_framework-0.1.0.dist-info/entry_points.txt +2 -0
  211. swiftpy_framework-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,1806 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import base64
5
+ import os
6
+ import re
7
+ import secrets
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ _NAME_PATTERN = re.compile(r"^[A-Za-z0-9_-]+$")
12
+
13
+ DOCKERFILE_CONTENT = """FROM python:3.12-slim-bookworm AS builder
14
+
15
+ WORKDIR /app
16
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
17
+ build-essential libpq-dev && \\
18
+ rm -rf /var/lib/apt/lists/*
19
+
20
+ COPY pyproject.toml .
21
+ RUN pip install --no-cache-dir --upgrade pip && \\
22
+ python -c 'import tomllib, subprocess, sys; subprocess.check_call([sys.executable, "-m", "pip", "wheel", "--no-cache-dir", "--wheel-dir", "/app/wheels", *tomllib.load(open("pyproject.toml", "rb"))["project"]["dependencies"]])'
23
+
24
+ FROM python:3.12-slim-bookworm
25
+
26
+ WORKDIR /app
27
+ RUN apt-get update && apt-get install -y --no-install-recommends \\
28
+ libpq5 curl && \\
29
+ rm -rf /var/lib/apt/lists/*
30
+
31
+ RUN groupadd -r swiftpy && useradd -r -g swiftpy swiftpy
32
+
33
+ COPY --from=builder /app/wheels /wheels
34
+ RUN pip install --no-cache-dir /wheels/*
35
+
36
+ COPY . .
37
+ RUN chown -R swiftpy:swiftpy /app
38
+
39
+ USER swiftpy
40
+ EXPOSE 8000
41
+
42
+ CMD ["swiftpy", "start", "--host", "0.0.0.0", "--port", "8000"]
43
+ """
44
+
45
+ DOCKER_COMPOSE_CONTENT = """services:
46
+ app:
47
+ build: .
48
+ ports:
49
+ - "127.0.0.1:8000:8000"
50
+ env_file:
51
+ - .env
52
+ volumes:
53
+ - .:/app
54
+ depends_on:
55
+ postgres:
56
+ condition: service_healthy
57
+ redis:
58
+ condition: service_healthy
59
+
60
+ postgres:
61
+ image: pgvector/pgvector:pg16
62
+ ports:
63
+ - "127.0.0.1:5432:5432"
64
+ environment:
65
+ POSTGRES_DB: ${DB_DATABASE:-swiftpy}
66
+ POSTGRES_USER: ${DB_USERNAME:-postgres}
67
+ POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
68
+ volumes:
69
+ - postgres_data:/var/lib/postgresql/data
70
+ healthcheck:
71
+ test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-postgres} -d ${DB_DATABASE:-swiftpy}"]
72
+ interval: 5s
73
+ timeout: 5s
74
+ retries: 5
75
+
76
+ redis:
77
+ image: redis:7-alpine
78
+ ports:
79
+ - "127.0.0.1:6379:6379"
80
+ volumes:
81
+ - redis_data:/data
82
+ healthcheck:
83
+ test: ["CMD", "redis-cli", "ping"]
84
+ interval: 5s
85
+ timeout: 5s
86
+ retries: 5
87
+
88
+ volumes:
89
+ postgres_data:
90
+ redis_data:
91
+ """
92
+
93
+ DOCKERIGNORE_CONTENT = """__pycache__
94
+ *.pyc
95
+ .venv
96
+ .env
97
+ .git
98
+ .gitignore
99
+ .pytest_cache
100
+ .mypy_cache
101
+ .ruff_cache
102
+ dist
103
+ build
104
+ storage/logs/*
105
+ storage/app/public/*
106
+ tests
107
+ """
108
+
109
+ GITIGNORE_CONTENT = """__pycache__/
110
+ *.py[cod]
111
+ *$py.class
112
+ .env
113
+ .venv/
114
+ env/
115
+ venv/
116
+ dist/
117
+ build/
118
+ *.egg-info/
119
+ .pytest_cache/
120
+ .mypy_cache/
121
+ .ruff_cache/
122
+ .coverage
123
+ htmlcov/
124
+ storage/logs/*.log
125
+ storage/app/public/*
126
+ !storage/app/public/.gitkeep
127
+ """
128
+
129
+ ENV_EXAMPLE_CONTENT = """# Application Configuration
130
+ APP_NAME=SwiftPY
131
+ APP_ENV=local
132
+ APP_KEY=
133
+ # Keep APP_DEBUG=false in staging/production; enable only for local debugging.
134
+ APP_DEBUG=false
135
+ APP_URL=http://localhost:8000
136
+
137
+ # Server (Uvicorn)
138
+ SERVER_HOST=127.0.0.1
139
+ SERVER_PORT=8000
140
+ SERVER_WORKERS=1
141
+ SERVER_RELOAD=true
142
+
143
+ # Database (PostgreSQL / asyncpg)
144
+ DB_CONNECTION=postgresql
145
+ DB_HOST=127.0.0.1
146
+ DB_PORT=5432
147
+ DB_DATABASE=swiftpy
148
+ DB_USERNAME=postgres
149
+ DB_PASSWORD=secret
150
+ DB_SSL=disable
151
+ DB_SSL_CA_FILE=
152
+ DB_POOL_MIN_SIZE=5
153
+ DB_POOL_MAX_SIZE=20
154
+ DB_POOL_MAX_QUERIES=50000
155
+ DB_POOL_MAX_INACTIVE_LIFETIME=300.0
156
+ DB_COMMAND_TIMEOUT=60.0
157
+ DB_CONNECT_TIMEOUT=10.0
158
+ DB_STATEMENT_TIMEOUT=30.0
159
+ DB_TRANSACTION_TIMEOUT=60.0
160
+
161
+ # Redis (Shared or dedicated instances)
162
+ REDIS_HOST=127.0.0.1
163
+ REDIS_PORT=6379
164
+ REDIS_PASSWORD=
165
+ REDIS_CACHE_DB=0
166
+ REDIS_QUEUE_DB=1
167
+ REDIS_EVENTS_DB=2
168
+
169
+ # Cache
170
+ CACHE_DRIVER=redis
171
+ CACHE_PREFIX=swiftpy_cache:
172
+ CACHE_DEFAULT_TTL=3600
173
+
174
+ # Events
175
+ EVENTS_DRIVER=redis
176
+ EVENTS_PREFIX=swiftpy_events:
177
+
178
+ # Queue
179
+ QUEUE_DRIVER=redis
180
+ QUEUE_PREFIX=swiftpy_queue:
181
+ QUEUE_NAME=default
182
+
183
+ # Filesystems & Storage
184
+ FILESYSTEM_DISK=local
185
+ STORAGE_LOCAL_ROOT=storage/app/private
186
+ STORAGE_PUBLIC_ROOT=storage/app/public
187
+ STORAGE_PUBLIC_URL=/storage
188
+ AWS_ACCESS_KEY_ID=
189
+ AWS_SECRET_ACCESS_KEY=
190
+ AWS_DEFAULT_REGION=us-east-1
191
+ AWS_BUCKET=
192
+ AWS_ENDPOINT=
193
+
194
+ # Mail Subsystem
195
+ MAIL_MAILER=smtp
196
+ MAIL_HOST=127.0.0.1
197
+ MAIL_PORT=587
198
+ MAIL_USERNAME=
199
+ MAIL_PASSWORD=
200
+ MAIL_ENCRYPTION=starttls
201
+ MAIL_TIMEOUT=30.0
202
+ MAIL_VALIDATE_CERTS=true
203
+ MAIL_FROM_ADDRESS=noreply@example.com
204
+ MAIL_FROM_NAME="${APP_NAME}"
205
+ RESEND_API_KEY=
206
+ SENDGRID_API_KEY=
207
+ MAILGUN_API_KEY=
208
+ MAILGUN_DOMAIN=
209
+
210
+ # Authentication & Security
211
+ AUTH_GUARD=jwt
212
+ AUTH_BLACKLIST_ENABLED=true
213
+ JWT_SECRET=
214
+ JWT_ALGORITHM=HS256
215
+ JWT_ACCESS_TTL=900
216
+ JWT_REFRESH_TTL=604800
217
+ AUTH_KEY_PEPPER=
218
+
219
+ # CORS (opt-in explicit comma-separated origins, e.g. http://localhost:3000,http://localhost:8000)
220
+ # Never pair '*' with CORS_ALLOW_CREDENTIALS=true
221
+ CORS_ALLOWED_ORIGINS=
222
+ CORS_ALLOW_CREDENTIALS=false
223
+ CORS_MAX_AGE=600
224
+
225
+ # Logging
226
+ LOG_CHANNEL=stack
227
+ LOG_LEVEL=info
228
+
229
+ # AI Subsystem
230
+ AI_ENABLED=true
231
+ AI_DEFAULT_PROVIDER=openai
232
+ OPENAI_API_KEY=
233
+ OPENAI_MODEL=gpt-4o
234
+ ANTHROPIC_API_KEY=
235
+ ANTHROPIC_MODEL=claude-3-5-sonnet-20241022
236
+ """
237
+
238
+ MAIN_PY_CONTENT = """from __future__ import annotations
239
+
240
+ from swiftpy.core.application import Application
241
+
242
+ from app.http.handler.exceptions import register_exception_handlers
243
+ from routes.api import router as api_router
244
+
245
+ app = Application()
246
+ register_exception_handlers(app)
247
+
248
+ # Enable opt-in CORS middleware (configured in config/cors.py):
249
+ # from swiftpy.http.middleware import CORSMiddleware
250
+ # app.middleware([CORSMiddleware])
251
+
252
+ # Mount default API routes under /api
253
+ app.include_router(api_router, prefix="/api")
254
+
255
+ # Mount custom modular route files as the project expands:
256
+ # from routes.auth import router as auth_router
257
+ # app.include_router(auth_router, prefix="/api/auth")
258
+ """
259
+
260
+ ROUTES_API_PY_CONTENT = """from __future__ import annotations
261
+
262
+ from swiftpy.http.request import Request
263
+ from swiftpy.http.response import Response, json_response
264
+ from swiftpy.http.router import Router
265
+
266
+ from app.schemas.user_schema import UserResponseSchema
267
+
268
+ router = Router()
269
+
270
+
271
+ @router.get("/health")
272
+ async def health_check(request: Request) -> Response:
273
+ \"\"\"Health check endpoint for container orchestrators and monitoring.\"\"\"
274
+ return json_response({"status": "ok", "app": "SwiftPY"})
275
+
276
+
277
+ @router.get("/users")
278
+ async def list_users(request: Request) -> Response:
279
+ \"\"\"Example user listing endpoint returning typed schema response.\"\"\"
280
+ return json_response(
281
+ [
282
+ UserResponseSchema(
283
+ id=1, name="Alice Doe", email="alice@example.com"
284
+ ).to_dict()
285
+ ]
286
+ )
287
+ """
288
+
289
+ USER_MODEL_CONTENT = """from __future__ import annotations
290
+
291
+ from datetime import datetime
292
+ from typing import ClassVar
293
+
294
+ from swiftpy.database.orm import Model
295
+
296
+
297
+ class User(Model):
298
+ \"\"\"
299
+ Default User ORM model representing application accounts.
300
+ Maps to the `users` database table.
301
+ \"\"\"
302
+
303
+ __table__ = "users"
304
+ __primary_key__ = "id"
305
+ __fillable__: ClassVar[list[str]] = ["name", "email", "password", "is_admin"]
306
+ __hidden__: ClassVar[list[str]] = ["password"]
307
+
308
+ id: int
309
+ name: str
310
+ email: str
311
+ password: str
312
+ is_admin: bool
313
+ created_at: datetime
314
+ updated_at: datetime
315
+ """
316
+
317
+ USER_SCHEMA_CONTENT = """from __future__ import annotations
318
+
319
+ from dataclasses import asdict, dataclass
320
+ from typing import Any
321
+
322
+
323
+ @dataclass(slots=True)
324
+ class UserCreateSchema:
325
+ \"\"\"Input validation DTO for user creation requests.\"\"\"
326
+
327
+ name: str
328
+ email: str
329
+ password: str
330
+
331
+
332
+ @dataclass(slots=True)
333
+ class UserResponseSchema:
334
+ \"\"\"Output serialization schema for user responses.\"\"\"
335
+
336
+ id: int
337
+ name: str
338
+ email: str
339
+
340
+ def to_dict(self) -> dict[str, Any]:
341
+ return asdict(self)
342
+ """
343
+
344
+ USER_SERVICE_CONTENT = """from __future__ import annotations
345
+
346
+ from app.models.user import User
347
+ from app.repositories.user_repository import UserRepository
348
+
349
+
350
+ class UserService:
351
+ \"\"\"
352
+ Domain business logic layer for user entity management.
353
+ Coordinates between repositories, transactions, events, and background notifications.
354
+ \"\"\"
355
+
356
+ def __init__(self, repository: UserRepository | None = None) -> None:
357
+ self.repository = repository or UserRepository()
358
+
359
+ async def create_user(
360
+ self, name: str, email: str, password_hash: str
361
+ ) -> User:
362
+ \"\"\"Create a new user account with hashed credentials.\"\"\"
363
+ user = await self.repository.create(
364
+ name=name,
365
+ email=email,
366
+ password=password_hash,
367
+ is_admin=False,
368
+ )
369
+ return user
370
+ """
371
+
372
+ USER_REPOSITORY_CONTENT = """from __future__ import annotations
373
+
374
+ from app.models.user import User
375
+
376
+
377
+ class UserRepository:
378
+ \"\"\"
379
+ Data access repository for User entities.
380
+ Encapsulates database operations and keeps query logic decoupled from business services.
381
+ \"\"\"
382
+
383
+ async def find_by_id(self, user_id: int) -> User | None:
384
+ \"\"\"Find a single user by primary key ID.\"\"\"
385
+ return await User.find(user_id)
386
+
387
+ async def find_by_email(self, email: str) -> User | None:
388
+ \"\"\"Find a single user by email address.\"\"\"
389
+ return await User.where("email", email).first()
390
+
391
+ async def create(self, **attributes: object) -> User:
392
+ \"\"\"Persist a new User model instance.\"\"\"
393
+ return await User.create(**attributes)
394
+ """
395
+
396
+ USER_REGISTERED_EVENT_CONTENT = """from __future__ import annotations
397
+
398
+ from dataclasses import dataclass
399
+
400
+
401
+ @dataclass(slots=True, frozen=True)
402
+ class UserRegistered:
403
+ \"\"\"
404
+ Domain event triggered when a new user registers an account.
405
+ \"\"\"
406
+
407
+ user_id: int
408
+ email: str
409
+ name: str
410
+ """
411
+
412
+ SEND_WELCOME_NOTIFICATION_CONTENT = """from __future__ import annotations
413
+
414
+ import logging
415
+
416
+ from app.events.user_registered import UserRegistered
417
+
418
+ logger = logging.getLogger(__name__)
419
+
420
+
421
+ class SendWelcomeNotification:
422
+ \"\"\"
423
+ Event listener that sends a welcome notification or queued job
424
+ when a UserRegistered event is dispatched.
425
+ \"\"\"
426
+
427
+ async def handle(self, event: UserRegistered) -> None:
428
+ logger.info(
429
+ "Handling UserRegistered event for %s (%s)",
430
+ event.name,
431
+ event.email,
432
+ )
433
+ """
434
+
435
+ SEND_NOTIFICATION_JOB_CONTENT = """from __future__ import annotations
436
+
437
+ import logging
438
+
439
+ from swiftpy.queue.job import Job
440
+
441
+ logger = logging.getLogger(__name__)
442
+
443
+
444
+ class SendNotificationJob(Job):
445
+ \"\"\"
446
+ Sample background queued job demonstrating asynchronous execution,
447
+ retries, exponential backoff, and transactional dispatching.
448
+ \"\"\"
449
+
450
+ tries: int = 3
451
+ backoff: int = 5
452
+
453
+ def __init__(self, user_id: int, message: str) -> None:
454
+ self.user_id = user_id
455
+ self.message = message
456
+
457
+ async def handle(self) -> None:
458
+ \"\"\"Execute the notification dispatch logic.\"\"\"
459
+ logger.info(
460
+ "Dispatching background notification to user %s: %s",
461
+ self.user_id,
462
+ self.message,
463
+ )
464
+ """
465
+
466
+ WELCOME_EMAIL_CONTENT = """from __future__ import annotations
467
+
468
+ from swiftpy.mail.mailable import Mailable
469
+ from swiftpy.mail.message import MailMessage
470
+
471
+
472
+ class WelcomeEmail(Mailable):
473
+ \"\"\"
474
+ Sample transactional email sent to newly registered users.
475
+ Renders views/mail/template.html with user context data.
476
+ \"\"\"
477
+
478
+ def __init__(self, name: str, to: str | list[str] | None = None) -> None:
479
+ super().__init__()
480
+ self.name = name
481
+ self.to_address = [to] if isinstance(to, str) else (to or [])
482
+
483
+ def build(self) -> MailMessage:
484
+ return MailMessage(
485
+ subject=f"Welcome to SwiftPY, {self.name}!",
486
+ to=self.to_address,
487
+ template="mail/template",
488
+ context={
489
+ "title": f"Welcome, {self.name}!",
490
+ "content": "Thank you for joining our platform. We are thrilled to have you here!",
491
+ "app_name": "SwiftPY",
492
+ },
493
+ )
494
+ """
495
+
496
+ EXCEPTIONS_CONTENT = """from __future__ import annotations
497
+
498
+ from swiftpy.core.application import Application
499
+ from swiftpy.core.exceptions import HttpException
500
+ from swiftpy.http.request import Request
501
+ from swiftpy.http.response import Response, json_response
502
+
503
+
504
+ class ResourceConflictException(HttpException):
505
+ \"\"\"Raised when an operation conflicts with existing server state (HTTP 409).\"\"\"
506
+
507
+ def __init__(self, message: str = "Resource conflict") -> None:
508
+ super().__init__(status_code=409, message=message)
509
+
510
+
511
+ def register_exception_handlers(app: Application) -> None:
512
+ \"\"\"Register custom application error renderers onto the application.\"\"\"
513
+
514
+ @app.exception_handler(ResourceConflictException)
515
+ async def handle_conflict(
516
+ request: Request, exc: ResourceConflictException
517
+ ) -> Response:
518
+ return json_response(
519
+ {"error": "conflict", "detail": exc.message}, status_code=409
520
+ )
521
+ """
522
+
523
+ AUTH_MIDDLEWARE_CONTENT = """from __future__ import annotations
524
+
525
+ from swiftpy.auth.auth_manager import AuthManager
526
+ from swiftpy.core.container import get_container
527
+ from swiftpy.core.context import Context
528
+ from swiftpy.core.exceptions import HttpException
529
+ from swiftpy.http.middleware import Handler, Middleware
530
+ from swiftpy.http.request import Request
531
+ from swiftpy.http.response import Response
532
+
533
+
534
+ class AuthMiddleware(Middleware):
535
+ \"\"\"
536
+ Authentication ASGI middleware for SwiftPY.
537
+
538
+ Validates incoming credentials through the application's AuthManager guard,
539
+ binds the authenticated user entity to `request.state["user"]` and
540
+ task-isolated `Context`, and raises HTTP 401 when authentication fails.
541
+ \"\"\"
542
+
543
+ async def handle(self, request: Request, next_handler: Handler) -> Response:
544
+ container = get_container()
545
+ auth_manager = (
546
+ container.resolve(AuthManager)
547
+ if container.has(AuthManager)
548
+ else AuthManager(container)
549
+ )
550
+ user = await auth_manager.authenticate(request)
551
+ if user is None:
552
+ raise HttpException(
553
+ status_code=401,
554
+ message="Unauthenticated: Missing or invalid credentials",
555
+ )
556
+
557
+ Context.set("auth.user", user)
558
+ Context.set("__swiftpy_current_user__", user)
559
+ request.state["user"] = user
560
+
561
+ return await next_handler(request)
562
+ """
563
+
564
+ ADMIN_MIDDLEWARE_CONTENT = """from __future__ import annotations
565
+
566
+ from collections.abc import Mapping
567
+
568
+ from swiftpy.core.context import Context
569
+ from swiftpy.core.exceptions import HttpException
570
+ from swiftpy.http.middleware import Handler, Middleware
571
+ from swiftpy.http.request import Request
572
+ from swiftpy.http.response import Response
573
+
574
+
575
+ class AdminMiddleware(Middleware):
576
+ \"\"\"
577
+ Authorization ASGI middleware for administrative access control.
578
+
579
+ Retrieves the authenticated user from task-isolated `Context` or `request.state`.
580
+ Verifies that the user exists and carries boolean `is_admin is True`, raising
581
+ HTTP 401 Unauthorized if unauthenticated, or HTTP 403 Forbidden if not an administrator.
582
+ \"\"\"
583
+
584
+ async def handle(self, request: Request, next_handler: Handler) -> Response:
585
+ user = Context.get("auth.user")
586
+ if user is None:
587
+ user = Context.get("__swiftpy_current_user__")
588
+ if user is None:
589
+ user = request.state.get("user")
590
+ if user is None:
591
+ raise HttpException(
592
+ status_code=401,
593
+ message="Unauthenticated: Please authenticate first",
594
+ )
595
+
596
+ is_admin = (
597
+ user.get("is_admin") is True
598
+ if isinstance(user, Mapping)
599
+ else getattr(user, "is_admin", None) is True
600
+ )
601
+ if not is_admin:
602
+ raise HttpException(
603
+ status_code=403,
604
+ message="Forbidden: Administrator access required",
605
+ )
606
+
607
+ return await next_handler(request)
608
+ """
609
+
610
+ CREATE_USER_REQUEST_CONTENT = """from __future__ import annotations
611
+
612
+ from swiftpy.validation.form_request import FormRequest
613
+
614
+
615
+ class CreateUserRequest(FormRequest):
616
+ \"\"\"
617
+ FormRequest validating inbound user registration payloads.
618
+ Encapsulates field rules, sanitization, and custom error messages.
619
+ \"\"\"
620
+
621
+ def rules(self) -> dict[str, str]:
622
+ return {
623
+ "name": "required|string|min:2|max:100",
624
+ "email": "required|email|max:255",
625
+ "password": "required|string|min:8",
626
+ }
627
+
628
+ def messages(self) -> dict[str, str]:
629
+ return {
630
+ "name.required": "Full name is required.",
631
+ "email.email": "Please provide a valid email address.",
632
+ "password.min": "Password must be at least 8 characters long.",
633
+ }
634
+ """
635
+
636
+ APP_SERVICE_PROVIDER_CONTENT = """from __future__ import annotations
637
+
638
+ from swiftpy.core.providers.base import ServiceProvider
639
+
640
+
641
+ class AppServiceProvider(ServiceProvider):
642
+ \"\"\"
643
+ Core application service provider.
644
+ Registers application singletons and utilities in the DI container.
645
+ \"\"\"
646
+
647
+ def register(self) -> None:
648
+ pass
649
+
650
+ def boot(self) -> None:
651
+ pass
652
+ """
653
+
654
+ AUTH_SERVICE_PROVIDER_CONTENT = """from __future__ import annotations
655
+
656
+ from swiftpy.auth import (
657
+ AuthManager,
658
+ CurrentUser,
659
+ JWTIssuer,
660
+ TokenBlacklist,
661
+ get_server_pepper,
662
+ )
663
+ from swiftpy.core.config import Config
664
+ from swiftpy.core.providers.base import ServiceProvider
665
+
666
+
667
+ class AuthServiceProvider(ServiceProvider):
668
+ \"\"\"
669
+ Register authentication services, token management, and task-scoped user resolution.
670
+ \"\"\"
671
+
672
+ def register(self) -> None:
673
+ self.container.singleton(
674
+ JWTIssuer,
675
+ lambda c: JWTIssuer.from_config(
676
+ c.resolve(Config) if c.has(Config) else None
677
+ ),
678
+ )
679
+ self.container.singleton(
680
+ TokenBlacklist,
681
+ lambda c: TokenBlacklist(
682
+ issuer=c.resolve(JWTIssuer) if c.has(JWTIssuer) else None,
683
+ config=c.resolve(Config) if c.has(Config) else None,
684
+ ),
685
+ )
686
+ self.container.singleton(AuthManager)
687
+ self.container.scoped(
688
+ CurrentUser,
689
+ lambda _: CurrentUser(),
690
+ )
691
+
692
+ def boot(self) -> None:
693
+ if self.container.has(Config):
694
+ cfg = self.container.resolve(Config)
695
+ app_env = str(cfg.get("app.env") or cfg.get("env") or "development").lower()
696
+ debug_val = (
697
+ cfg.get("app.debug")
698
+ if cfg.get("app.debug") is not None
699
+ else cfg.get("debug")
700
+ )
701
+ is_debug = (
702
+ bool(debug_val)
703
+ if debug_val is not None
704
+ else app_env not in ("production", "prod")
705
+ )
706
+ default_guard = str(
707
+ cfg.get("auth.defaults.guard") or cfg.get("auth.default") or "jwt"
708
+ ).lower()
709
+ if not is_debug:
710
+ if default_guard == "jwt" and self.container.has(JWTIssuer):
711
+ self.container.resolve(JWTIssuer)
712
+ elif default_guard == "api_key":
713
+ get_server_pepper(cfg)
714
+ """
715
+
716
+ CACHE_SERVICE_PROVIDER_CONTENT = """from __future__ import annotations
717
+
718
+ from swiftpy.cache import Cache
719
+ from swiftpy.core.providers.base import ServiceProvider
720
+
721
+
722
+ class CacheServiceProvider(ServiceProvider):
723
+ \"\"\"
724
+ Register the Cache manager singleton into the application DI container.
725
+ \"\"\"
726
+
727
+ def register(self) -> None:
728
+ self.container.singleton(Cache)
729
+
730
+ def boot(self) -> None:
731
+ pass
732
+ """
733
+
734
+ DATABASE_SERVICE_PROVIDER_CONTENT = """from __future__ import annotations
735
+
736
+ from swiftpy.core.providers.base import ServiceProvider
737
+ from swiftpy.database.pool import DatabasePool
738
+
739
+
740
+ class DatabaseServiceProvider(ServiceProvider):
741
+ \"\"\"
742
+ Governs database connection pools, transaction events, and ORM lifecycle hooks.
743
+ \"\"\"
744
+
745
+ def register(self) -> None:
746
+ self.container.singleton(DatabasePool)
747
+
748
+ def boot(self) -> None:
749
+ pass
750
+ """
751
+
752
+ EVENT_SERVICE_PROVIDER_CONTENT = """from __future__ import annotations
753
+
754
+ from typing import ClassVar
755
+
756
+ from swiftpy.core.providers.base import ServiceProvider
757
+ from swiftpy.events import EventDispatcher, RedisEventBus
758
+
759
+ from app.events.user_registered import UserRegistered
760
+ from app.listeners.send_welcome_notification import SendWelcomeNotification
761
+
762
+
763
+ class EventServiceProvider(ServiceProvider):
764
+ \"\"\"
765
+ Wires domain events to their asynchronous listener classes.
766
+ \"\"\"
767
+
768
+ LISTENERS: ClassVar[dict[type, list[type]]] = {
769
+ UserRegistered: [SendWelcomeNotification],
770
+ }
771
+
772
+ def register(self) -> None:
773
+ self.container.singleton(
774
+ EventDispatcher, lambda _: EventDispatcher()
775
+ )
776
+ self.container.singleton(
777
+ RedisEventBus,
778
+ lambda c: RedisEventBus(
779
+ dispatcher=c.resolve(EventDispatcher),
780
+ ),
781
+ )
782
+
783
+ def boot(self) -> None:
784
+ dispatcher = self.container.resolve(EventDispatcher)
785
+ for event_cls, listener_classes in self.LISTENERS.items():
786
+ for listener_cls in listener_classes:
787
+ listener_inst = listener_cls()
788
+ dispatcher.listen(event_cls, listener_inst.handle)
789
+ """
790
+
791
+ FILESYSTEM_SERVICE_PROVIDER_CONTENT = """from __future__ import annotations
792
+
793
+ from swiftpy.core.providers.base import ServiceProvider
794
+ from swiftpy.filesystem.manager import FilesystemManager
795
+
796
+
797
+ class FilesystemServiceProvider(ServiceProvider):
798
+ \"\"\"
799
+ Registers the FilesystemManager singleton into the application DI container.
800
+ \"\"\"
801
+
802
+ def register(self) -> None:
803
+ self.container.singleton(FilesystemManager)
804
+
805
+ def boot(self) -> None:
806
+ pass
807
+ """
808
+
809
+ QUEUE_SERVICE_PROVIDER_CONTENT = """from __future__ import annotations
810
+
811
+ from swiftpy.core.providers.base import ServiceProvider
812
+ from swiftpy.queue.dispatcher import QueueDispatcher
813
+
814
+
815
+ class QueueServiceProvider(ServiceProvider):
816
+ \"\"\"
817
+ Register the QueueDispatcher singleton into the application DI container.
818
+ \"\"\"
819
+
820
+ def register(self) -> None:
821
+ self.container.singleton(
822
+ QueueDispatcher, lambda _: QueueDispatcher()
823
+ )
824
+
825
+ def boot(self) -> None:
826
+ pass
827
+ """
828
+
829
+ MAIL_SERVICE_PROVIDER_CONTENT = """from __future__ import annotations
830
+
831
+ from swiftpy.core.config import Config
832
+ from swiftpy.core.providers.base import ServiceProvider
833
+ from swiftpy.mail.manager import MailManager
834
+
835
+
836
+ class MailServiceProvider(ServiceProvider):
837
+ \"\"\"
838
+ Register the MailManager singleton into the application DI container.
839
+ \"\"\"
840
+
841
+ def register(self) -> None:
842
+ self.container.singleton(
843
+ MailManager,
844
+ lambda c: MailManager(
845
+ config=c.resolve(Config) if c.has(Config) else None
846
+ ),
847
+ )
848
+
849
+ async def shutdown(self) -> None:
850
+ manager = self.container.get_singleton(MailManager)
851
+ if isinstance(manager, MailManager):
852
+ await manager.close()
853
+ """
854
+
855
+ CREATE_USERS_MIGRATION_CONTENT = """from __future__ import annotations
856
+
857
+ from swiftpy.database.migrations import Blueprint, Migration, Schema
858
+
859
+
860
+ class CreateUsersTable(Migration):
861
+ async def up(self) -> None:
862
+ def define(table: Blueprint) -> None:
863
+ table.id()
864
+ table.string("name")
865
+ table.string("email").unique()
866
+ table.string("password")
867
+ table.boolean("is_admin").default(False)
868
+ table.timestamps()
869
+
870
+ await Schema.create("users", define)
871
+
872
+ async def down(self) -> None:
873
+ await Schema.drop_if_exists("users")
874
+ """
875
+
876
+ DATABASE_SEEDER_CONTENT = """from __future__ import annotations
877
+
878
+ from swiftpy.database.seeder import Seeder
879
+
880
+
881
+ class DatabaseSeeder(Seeder):
882
+ \"\"\"
883
+ Root database seeder.
884
+ Run via `swiftpy db:seed`. Call additional seeders using `await self.call(...)`.
885
+ \"\"\"
886
+
887
+ async def run(self) -> None:
888
+ # Example:
889
+ # await self.call(UserSeeder)
890
+ pass
891
+ """
892
+
893
+ CONFIG_AI_CONTENT = """from __future__ import annotations
894
+
895
+ from swiftpy.helpers.helper import env, env_bool
896
+
897
+ CONFIG = {
898
+ "enabled": env_bool("AI_ENABLED", True),
899
+ "default": env("AI_DEFAULT_PROVIDER", "openai"),
900
+ "providers": {
901
+ "openai": {
902
+ "api_key": env("OPENAI_API_KEY", ""),
903
+ "model": env("OPENAI_MODEL", "gpt-4o"),
904
+ },
905
+ "anthropic": {
906
+ "api_key": env("ANTHROPIC_API_KEY", ""),
907
+ "model": env("ANTHROPIC_MODEL", "claude-3-5-sonnet-20241022"),
908
+ },
909
+ },
910
+ }
911
+ """
912
+
913
+ CONFIG_APP_CONTENT = """from __future__ import annotations
914
+
915
+ from swiftpy.helpers.helper import env, env_bool
916
+
917
+ CONFIG = {
918
+ "name": env("APP_NAME", "SwiftPY"),
919
+ "env": env("APP_ENV", "local"),
920
+ "debug": env_bool("APP_DEBUG", False),
921
+ "enable_benchmarks": env_bool("APP_ENABLE_BENCHMARKS", False),
922
+ "key": env("APP_KEY", ""),
923
+ "url": env("APP_URL", "http://localhost:8000"),
924
+ "cipher": "AES-256-GCM",
925
+ }
926
+ """
927
+
928
+ CONFIG_AUTH_CONTENT = """from __future__ import annotations
929
+
930
+ from swiftpy.helpers.helper import env, env_bool
931
+
932
+ CONFIG = {
933
+ "defaults": {
934
+ "guard": env("AUTH_GUARD", "jwt"),
935
+ },
936
+ "blacklist_enabled": env_bool("AUTH_BLACKLIST_ENABLED", True),
937
+ "guards": {
938
+ "jwt": {
939
+ "secret": env("JWT_SECRET", ""),
940
+ "algorithm": env("JWT_ALGORITHM", "HS256"),
941
+ "access_token_ttl": int(env("JWT_ACCESS_TTL", "900") or "900"),
942
+ "refresh_token_ttl": int(env("JWT_REFRESH_TTL", "604800") or "604800"),
943
+ "leeway": int(env("JWT_LEEWAY", "0") or "0"),
944
+ "issuer": env("JWT_ISSUER") or None,
945
+ "audience": env("JWT_AUDIENCE") or None,
946
+ "private_key": env("JWT_PRIVATE_KEY") or None,
947
+ "public_key": env("JWT_PUBLIC_KEY") or None,
948
+ },
949
+ "api_key": {
950
+ "table": env("AUTH_API_KEYS_TABLE", "api_keys"),
951
+ "prefix_column": "prefix",
952
+ "hash_column": "key_hash",
953
+ "header": "X-API-Key",
954
+ "pepper": env("AUTH_KEY_PEPPER", ""),
955
+ },
956
+ },
957
+ "redis": {
958
+ "host": env("REDIS_HOST", "127.0.0.1"),
959
+ "port": int(env("REDIS_PORT", "6379") or "6379"),
960
+ "password": env("REDIS_PASSWORD") or None,
961
+ "database": int(env("REDIS_AUTH_DB", env("REDIS_CACHE_DB", "0")) or "0"),
962
+ },
963
+ }
964
+ """
965
+
966
+ CONFIG_CACHE_CONTENT = """from __future__ import annotations
967
+
968
+ from swiftpy.helpers.helper import env
969
+
970
+ CONFIG = {
971
+ "default": env("CACHE_DRIVER", "redis"),
972
+ "prefix": env("CACHE_PREFIX", "swiftpy_cache:"),
973
+ "default_ttl": int(env("CACHE_DEFAULT_TTL", "3600") or "3600"),
974
+ "stores": {
975
+ "memory": {
976
+ "driver": "memory",
977
+ "prefix": env("CACHE_PREFIX", "swiftpy_cache:"),
978
+ "ttl": int(env("CACHE_DEFAULT_TTL", "3600") or "3600"),
979
+ },
980
+ "redis": {
981
+ "driver": "redis",
982
+ "host": env("REDIS_HOST", "127.0.0.1"),
983
+ "port": int(env("REDIS_PORT", "6379") or "6379"),
984
+ "password": env("REDIS_PASSWORD") or None,
985
+ "database": int(env("REDIS_CACHE_DB", "0") or "0"),
986
+ "prefix": env("CACHE_PREFIX", "swiftpy_cache:"),
987
+ "ttl": int(env("CACHE_DEFAULT_TTL", "3600") or "3600"),
988
+ },
989
+ },
990
+ }
991
+ """
992
+
993
+ CONFIG_CORS_CONTENT = """from __future__ import annotations
994
+
995
+ from swiftpy.helpers.helper import env, env_bool
996
+
997
+ CONFIG = {
998
+ "allowed_origins": [
999
+ origin.strip()
1000
+ for origin in str(env("CORS_ALLOWED_ORIGINS", "") or "").split(",")
1001
+ if origin.strip()
1002
+ ],
1003
+ "allowed_methods": ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"],
1004
+ "allowed_headers": ["Content-Type", "Authorization", "X-Request-ID"],
1005
+ "expose_headers": ["X-Request-ID"],
1006
+ "allow_credentials": env_bool("CORS_ALLOW_CREDENTIALS", False),
1007
+ "max_age": int(env("CORS_MAX_AGE", "600") or "600"),
1008
+ }
1009
+ """
1010
+
1011
+ CONFIG_DATABASE_CONTENT = """from __future__ import annotations
1012
+
1013
+ from swiftpy.helpers.helper import env
1014
+
1015
+ CONFIG = {
1016
+ "default": env("DB_CONNECTION", "postgresql"),
1017
+ "connections": {
1018
+ "postgresql": {
1019
+ "host": env("DB_HOST", "127.0.0.1"),
1020
+ "port": int(env("DB_PORT", "5432") or "5432"),
1021
+ "database": env("DB_DATABASE", "swiftpy"),
1022
+ "user": env("DB_USERNAME", "postgres"),
1023
+ "password": env("DB_PASSWORD", "secret"),
1024
+ "ssl": env("DB_SSL", "disable"),
1025
+ "ssl_ca_file": env("DB_SSL_CA_FILE") or None,
1026
+ },
1027
+ },
1028
+ "pool": {
1029
+ "min_size": int(env("DB_POOL_MIN_SIZE", "5") or "5"),
1030
+ "max_size": int(env("DB_POOL_MAX_SIZE", "20") or "20"),
1031
+ "max_queries": int(env("DB_POOL_MAX_QUERIES", "50000") or "50000"),
1032
+ "max_inactive_connection_lifetime": float(
1033
+ env("DB_POOL_MAX_INACTIVE_LIFETIME", "300.0") or "300.0"
1034
+ ),
1035
+ "command_timeout": float(env("DB_COMMAND_TIMEOUT", "60.0") or "60.0"),
1036
+ },
1037
+ "timeouts": {
1038
+ "connect": float(env("DB_CONNECT_TIMEOUT", "10.0") or "10.0"),
1039
+ "statement": float(env("DB_STATEMENT_TIMEOUT", "30.0") or "30.0"),
1040
+ "transaction": float(env("DB_TRANSACTION_TIMEOUT", "60.0") or "60.0"),
1041
+ },
1042
+ }
1043
+ """
1044
+
1045
+ CONFIG_EVENTS_CONTENT = """from __future__ import annotations
1046
+
1047
+ from swiftpy.helpers.helper import env
1048
+
1049
+ CONFIG = {
1050
+ "default": env("EVENTS_DRIVER", "redis"),
1051
+ "redis": {
1052
+ "host": env("REDIS_HOST", "127.0.0.1"),
1053
+ "port": int(env("REDIS_PORT", "6379") or "6379"),
1054
+ "password": env("REDIS_PASSWORD") or None,
1055
+ "database": int(env("REDIS_EVENTS_DB", "2") or "2"),
1056
+ "prefix": env("EVENTS_PREFIX", "swiftpy_events:"),
1057
+ },
1058
+ }
1059
+ """
1060
+
1061
+ CONFIG_FILESYSTEMS_CONTENT = """from __future__ import annotations
1062
+
1063
+ from swiftpy.helpers.helper import env
1064
+
1065
+ CONFIG = {
1066
+ "default": env("FILESYSTEM_DISK", "local"),
1067
+ "signing_key": env("FILESYSTEM_SIGNING_KEY") or None,
1068
+ "disks": {
1069
+ "local": {
1070
+ "driver": "local",
1071
+ "root": env("STORAGE_LOCAL_ROOT", "storage/app/private"),
1072
+ "visibility": "private",
1073
+ },
1074
+ "public": {
1075
+ "driver": "local",
1076
+ "root": env("STORAGE_PUBLIC_ROOT", "storage/app/public"),
1077
+ "url": env("STORAGE_PUBLIC_URL", "/storage"),
1078
+ "visibility": "public",
1079
+ },
1080
+ "s3": {
1081
+ "driver": "s3",
1082
+ "key": env("AWS_ACCESS_KEY_ID", ""),
1083
+ "secret": env("AWS_SECRET_ACCESS_KEY", ""),
1084
+ "region": env("AWS_DEFAULT_REGION", "us-east-1"),
1085
+ "bucket": env("AWS_BUCKET", ""),
1086
+ "endpoint": env("AWS_ENDPOINT") or None,
1087
+ "visibility": "private",
1088
+ },
1089
+ },
1090
+ }
1091
+ """
1092
+
1093
+ CONFIG_LOGGING_CONTENT = """from __future__ import annotations
1094
+
1095
+ from swiftpy.helpers.helper import env
1096
+
1097
+ CONFIG = {
1098
+ "default": env("LOG_CHANNEL", "stack"),
1099
+ "level": env("LOG_LEVEL", "info"),
1100
+ "channels": {
1101
+ "stack": {
1102
+ "driver": "stack",
1103
+ "channels": ["daily"],
1104
+ },
1105
+ "single": {
1106
+ "driver": "single",
1107
+ "path": "storage/logs/swiftpy.log",
1108
+ "level": env("LOG_LEVEL", "info"),
1109
+ },
1110
+ "daily": {
1111
+ "driver": "daily",
1112
+ "path": "storage/logs/swiftpy.log",
1113
+ "level": env("LOG_LEVEL", "info"),
1114
+ "days": 14,
1115
+ },
1116
+ },
1117
+ }
1118
+ """
1119
+
1120
+ CONFIG_MAIL_CONTENT = """from __future__ import annotations
1121
+
1122
+ from swiftpy.helpers.helper import env, env_bool
1123
+
1124
+ CONFIG = {
1125
+ "default": env("MAIL_MAILER", "smtp"),
1126
+ "from": {
1127
+ "address": env("MAIL_FROM_ADDRESS", "noreply@example.com"),
1128
+ "name": env("MAIL_FROM_NAME", "SwiftPY"),
1129
+ },
1130
+ "mailers": {
1131
+ "smtp": {
1132
+ "transport": "smtp",
1133
+ "host": env("MAIL_HOST", "127.0.0.1"),
1134
+ "port": int(env("MAIL_PORT", "587") or "587"),
1135
+ "username": env("MAIL_USERNAME") or None,
1136
+ "password": env("MAIL_PASSWORD") or None,
1137
+ "encryption": env("MAIL_ENCRYPTION", "starttls"),
1138
+ "timeout": float(env("MAIL_TIMEOUT", "30.0") or "30.0"),
1139
+ "validate_certs": env_bool("MAIL_VALIDATE_CERTS", True),
1140
+ },
1141
+ "resend": {
1142
+ "transport": "resend",
1143
+ "api_key": env("RESEND_API_KEY", ""),
1144
+ },
1145
+ "sendgrid": {
1146
+ "transport": "sendgrid",
1147
+ "api_key": env("SENDGRID_API_KEY", ""),
1148
+ },
1149
+ "mailgun": {
1150
+ "transport": "mailgun",
1151
+ "api_key": env("MAILGUN_API_KEY", ""),
1152
+ "domain": env("MAILGUN_DOMAIN", ""),
1153
+ },
1154
+ "sendmail": {
1155
+ "transport": "sendmail",
1156
+ "path": "/usr/sbin/sendmail",
1157
+ },
1158
+ "console": {
1159
+ "transport": "console",
1160
+ },
1161
+ "fake": {
1162
+ "transport": "fake",
1163
+ },
1164
+ },
1165
+ }
1166
+ """
1167
+
1168
+ CONFIG_QUEUE_CONTENT = """from __future__ import annotations
1169
+
1170
+ from swiftpy.helpers.helper import env
1171
+
1172
+ CONFIG = {
1173
+ "default": env("QUEUE_DRIVER", "redis"),
1174
+ "connections": {
1175
+ "redis": {
1176
+ "driver": "redis",
1177
+ "host": env("REDIS_HOST", "127.0.0.1"),
1178
+ "port": int(env("REDIS_PORT", "6379") or "6379"),
1179
+ "password": env("REDIS_PASSWORD") or None,
1180
+ "database": int(env("REDIS_QUEUE_DB", "1") or "1"),
1181
+ "queue": env("QUEUE_NAME", "default"),
1182
+ "prefix": env("QUEUE_PREFIX", "swiftpy_queue:"),
1183
+ },
1184
+ },
1185
+ }
1186
+ """
1187
+
1188
+ VIEWS_MAIL_TEMPLATE_CONTENT = """<!DOCTYPE html>
1189
+ <html lang="en">
1190
+ <head>
1191
+ <meta charset="UTF-8">
1192
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1193
+ <title>{{ title }}</title>
1194
+ <style>
1195
+ body {
1196
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
1197
+ background-color: #f4f4f7;
1198
+ color: #51545e;
1199
+ margin: 0;
1200
+ padding: 0;
1201
+ width: 100% !important;
1202
+ -webkit-text-size-adjust: none;
1203
+ }
1204
+ .email-wrapper {
1205
+ width: 100%;
1206
+ margin: 0;
1207
+ padding: 20px 0;
1208
+ background-color: #f4f4f7;
1209
+ }
1210
+ .email-content {
1211
+ max-width: 600px;
1212
+ margin: 0 auto;
1213
+ background-color: #ffffff;
1214
+ border-radius: 8px;
1215
+ overflow: hidden;
1216
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
1217
+ }
1218
+ .email-header {
1219
+ background-color: #1a1a2e;
1220
+ padding: 25px;
1221
+ text-align: center;
1222
+ }
1223
+ .email-header h1 {
1224
+ color: #ffffff;
1225
+ font-size: 20px;
1226
+ font-weight: 700;
1227
+ margin: 0;
1228
+ }
1229
+ .email-body {
1230
+ padding: 35px;
1231
+ line-height: 1.6;
1232
+ }
1233
+ .email-footer {
1234
+ padding: 20px;
1235
+ text-align: center;
1236
+ font-size: 12px;
1237
+ color: #a8aaaf;
1238
+ }
1239
+ </style>
1240
+ </head>
1241
+ <body>
1242
+ <div class="email-wrapper">
1243
+ <div class="email-content">
1244
+ <div class="email-header">
1245
+ <h1>{{ app_name }}</h1>
1246
+ </div>
1247
+ <div class="email-body">
1248
+ <h2>{{ title }}</h2>
1249
+ <p>{{ content }}</p>
1250
+ </div>
1251
+ <div class="email-footer">
1252
+ <p>&copy; {{ app_name }}. All rights reserved.</p>
1253
+ </div>
1254
+ </div>
1255
+ </div>
1256
+ </body>
1257
+ </html>
1258
+ """
1259
+
1260
+ VIEWS_ADMIN_LOGIN_CONTENT = """<!DOCTYPE html>
1261
+ <html lang="en">
1262
+ <head>
1263
+ <meta charset="UTF-8">
1264
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1265
+ <title>Admin Login - SwiftPY</title>
1266
+ <style>
1267
+ body {
1268
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1269
+ background: #0f172a;
1270
+ color: #e2e8f0;
1271
+ display: flex;
1272
+ align-items: center;
1273
+ justify-content: center;
1274
+ min-height: 100vh;
1275
+ margin: 0;
1276
+ }
1277
+ .card {
1278
+ background: #1e293b;
1279
+ padding: 2.5rem;
1280
+ border-radius: 12px;
1281
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
1282
+ width: 100%;
1283
+ max-width: 400px;
1284
+ }
1285
+ h1 {
1286
+ font-size: 1.5rem;
1287
+ margin-bottom: 1.5rem;
1288
+ text-align: center;
1289
+ color: #38bdf8;
1290
+ }
1291
+ .form-group {
1292
+ margin-bottom: 1.25rem;
1293
+ }
1294
+ label {
1295
+ display: block;
1296
+ margin-bottom: 0.5rem;
1297
+ font-size: 0.875rem;
1298
+ color: #94a3b8;
1299
+ }
1300
+ input {
1301
+ width: 100%;
1302
+ padding: 0.75rem;
1303
+ border-radius: 6px;
1304
+ border: 1px solid #334155;
1305
+ background: #0f172a;
1306
+ color: #f8fafc;
1307
+ box-sizing: border-box;
1308
+ }
1309
+ input:focus {
1310
+ outline: none;
1311
+ border-color: #38bdf8;
1312
+ }
1313
+ button {
1314
+ width: 100%;
1315
+ padding: 0.75rem;
1316
+ border-radius: 6px;
1317
+ border: none;
1318
+ background: #0284c7;
1319
+ color: #fff;
1320
+ font-weight: 600;
1321
+ cursor: pointer;
1322
+ transition: background 0.2s;
1323
+ }
1324
+ button:hover {
1325
+ background: #0369a1;
1326
+ }
1327
+ </style>
1328
+ </head>
1329
+ <body>
1330
+ <div class="card">
1331
+ <h1>SwiftPY Admin</h1>
1332
+ <form method="POST" action="/admin/login">
1333
+ <div class="form-group">
1334
+ <label for="username">Username</label>
1335
+ <input type="text" id="username" name="username" required autocomplete="username">
1336
+ </div>
1337
+ <div class="form-group">
1338
+ <label for="password">Password</label>
1339
+ <input type="password" id="password" name="password" required autocomplete="current-password">
1340
+ </div>
1341
+ <button type="submit">Sign In</button>
1342
+ </form>
1343
+ </div>
1344
+ </body>
1345
+ </html>
1346
+ """
1347
+
1348
+ VIEWS_ADMIN_DASHBOARD_CONTENT = """<!DOCTYPE html>
1349
+ <html lang="en">
1350
+ <head>
1351
+ <meta charset="UTF-8">
1352
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1353
+ <title>Dashboard - SwiftPY Admin</title>
1354
+ <style>
1355
+ body {
1356
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
1357
+ background: #0f172a;
1358
+ color: #e2e8f0;
1359
+ margin: 0;
1360
+ display: flex;
1361
+ min-height: 100vh;
1362
+ }
1363
+ .sidebar {
1364
+ width: 250px;
1365
+ background: #1e293b;
1366
+ padding: 2rem 1.5rem;
1367
+ border-right: 1px solid #334155;
1368
+ }
1369
+ .sidebar h2 {
1370
+ font-size: 1.25rem;
1371
+ color: #38bdf8;
1372
+ margin-top: 0;
1373
+ margin-bottom: 2rem;
1374
+ }
1375
+ .nav-link {
1376
+ display: block;
1377
+ color: #94a3b8;
1378
+ text-decoration: none;
1379
+ padding: 0.75rem 1rem;
1380
+ border-radius: 6px;
1381
+ margin-bottom: 0.5rem;
1382
+ transition: all 0.2s;
1383
+ }
1384
+ .nav-link:hover, .nav-link.active {
1385
+ background: #334155;
1386
+ color: #f8fafc;
1387
+ }
1388
+ .main-content {
1389
+ flex: 1;
1390
+ padding: 2.5rem;
1391
+ }
1392
+ .header {
1393
+ display: flex;
1394
+ justify-content: space-between;
1395
+ align-items: center;
1396
+ margin-bottom: 2rem;
1397
+ }
1398
+ .card-grid {
1399
+ display: grid;
1400
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
1401
+ gap: 1.5rem;
1402
+ }
1403
+ .card {
1404
+ background: #1e293b;
1405
+ padding: 1.5rem;
1406
+ border-radius: 8px;
1407
+ border: 1px solid #334155;
1408
+ }
1409
+ .card h3 {
1410
+ margin-top: 0;
1411
+ font-size: 0.875rem;
1412
+ color: #94a3b8;
1413
+ text-transform: uppercase;
1414
+ }
1415
+ .card .value {
1416
+ font-size: 1.75rem;
1417
+ font-weight: 700;
1418
+ color: #f8fafc;
1419
+ }
1420
+ </style>
1421
+ </head>
1422
+ <body>
1423
+ <div class="sidebar">
1424
+ <h2>SwiftPY Admin</h2>
1425
+ <nav>
1426
+ <a href="/admin" class="nav-link active">Dashboard</a>
1427
+ <a href="/admin/users" class="nav-link">Users</a>
1428
+ <a href="/admin/logs" class="nav-link">Logs</a>
1429
+ <a href="/admin/settings" class="nav-link">Settings</a>
1430
+ </nav>
1431
+ </div>
1432
+ <div class="main-content">
1433
+ <div class="header">
1434
+ <h1>System Overview</h1>
1435
+ </div>
1436
+ <div class="card-grid">
1437
+ <div class="card">
1438
+ <h3>System Status</h3>
1439
+ <div class="value">Operational</div>
1440
+ </div>
1441
+ <div class="card">
1442
+ <h3>Version</h3>
1443
+ <div class="value">1.0.0</div>
1444
+ </div>
1445
+ </div>
1446
+ </div>
1447
+ </body>
1448
+ </html>
1449
+ """
1450
+
1451
+ TESTS_CONFTEST_CONTENT = """from __future__ import annotations
1452
+
1453
+ import os
1454
+ from collections.abc import AsyncGenerator, Generator
1455
+
1456
+ import pytest
1457
+
1458
+ # Ensure testing environment
1459
+ os.environ["APP_ENV"] = "testing"
1460
+ os.environ["MAIL_MAILER"] = "fake"
1461
+
1462
+ from swiftpy.mail import Mail
1463
+ from swiftpy.testing.http_client import TestClient
1464
+
1465
+ from main import app
1466
+
1467
+
1468
+ @pytest.fixture
1469
+ async def client() -> AsyncGenerator[TestClient, None]:
1470
+ \"\"\"Test client fixture for making requests against the application.\"\"\"
1471
+ async with TestClient(app) as test_client:
1472
+ yield test_client
1473
+
1474
+
1475
+ @pytest.fixture(autouse=True)
1476
+ def fake_mail() -> Generator[object, None, None]:
1477
+ \"\"\"Automatically fake mail subsystem during test runs.\"\"\"
1478
+ with Mail.fake() as fake:
1479
+ yield fake
1480
+ """
1481
+
1482
+ TESTS_TEST_API_CONTENT = """from __future__ import annotations
1483
+
1484
+ import pytest
1485
+ from swiftpy.testing.http_client import TestClient
1486
+
1487
+
1488
+ @pytest.mark.asyncio
1489
+ async def test_health_check_endpoint(client: TestClient) -> None:
1490
+ response = await client.get("/api/health")
1491
+ assert response.status_code == 200
1492
+ assert response.json()["status"] == "ok"
1493
+ """
1494
+
1495
+
1496
+ def _get_pyproject_toml(project_name: str) -> str:
1497
+ return f"""[build-system]
1498
+ requires = ["setuptools>=75.0"]
1499
+ build-backend = "setuptools.build_meta"
1500
+
1501
+ [project]
1502
+ name = "{project_name}"
1503
+ version = "0.1.0"
1504
+ description = "A SwiftPY web application"
1505
+ requires-python = ">=3.12"
1506
+ dependencies = [
1507
+ "swiftpy-framework>=0.1.0",
1508
+ "uvicorn[standard]>=0.52.4",
1509
+ "asyncpg>=0.30.0",
1510
+ "redis>=8.1.0",
1511
+ ]
1512
+
1513
+ [dependency-groups]
1514
+ dev = [
1515
+ "pytest>=9.1.1",
1516
+ "pytest-asyncio>=1.4.0",
1517
+ "mypy>=2.3.1",
1518
+ "ruff>=0.16.4",
1519
+ ]
1520
+
1521
+ [tool.setuptools]
1522
+ py-modules = ["main"]
1523
+
1524
+ [tool.setuptools.packages.find]
1525
+ include = ["app*", "config*", "database*", "routes*"]
1526
+
1527
+ [tool.pytest.ini_options]
1528
+ asyncio_mode = "auto"
1529
+ testpaths = ["tests"]
1530
+
1531
+ [tool.mypy]
1532
+ strict = true
1533
+ explicit_package_bases = true
1534
+ follow_imports = "silent"
1535
+ ignore_missing_imports = true
1536
+ """
1537
+
1538
+
1539
+ def _get_readme(project_name: str) -> str:
1540
+ return f"""# {project_name}
1541
+
1542
+ A high-performance Python ASGI backend powered by the [SwiftPY](https://github.com/swiftpy/swiftpy) framework.
1543
+
1544
+ ## Prerequisites
1545
+
1546
+ - Python >= 3.12
1547
+ - PostgreSQL 16 (with `pgvector` extension if using AI vector stores)
1548
+ - Redis 7
1549
+
1550
+ ## Getting Started
1551
+
1552
+ 1. Install dependencies using `uv` (recommended) or `pip`:
1553
+ ```bash
1554
+ uv sync
1555
+ source .venv/bin/activate
1556
+ ```
1557
+ Or with standard `venv` + `pip`:
1558
+ ```bash
1559
+ python -m venv .venv
1560
+ source .venv/bin/activate
1561
+ pip install -e .
1562
+ ```
1563
+
1564
+ 2. Environment configuration:
1565
+ The `.env` file has been automatically initialized with a secure `APP_KEY`.
1566
+ Update database and Redis credentials in `.env` if needed.
1567
+
1568
+ 3. Run outstanding database migrations:
1569
+ ```bash
1570
+ swiftpy migrate
1571
+ ```
1572
+
1573
+ 4. Create symbolic link for public storage files:
1574
+ ```bash
1575
+ swiftpy storage:link
1576
+ ```
1577
+
1578
+ 5. Start the development server with live reload:
1579
+ ```bash
1580
+ swiftpy start --reload
1581
+ ```
1582
+
1583
+ ## Development & CLI Commands
1584
+
1585
+ - `swiftpy make:model <Name> -m`: Create model and migration
1586
+ - `swiftpy make:controller <Name>`: Create an HTTP controller
1587
+ - `swiftpy make:service <Name>`: Create a domain service
1588
+ - `swiftpy make:repository <Name>`: Create a data repository
1589
+ - `swiftpy make:schema <Name>`: Create an input/output schema
1590
+ - `swiftpy make:seeder <Name>`: Create a database seeder
1591
+ - `swiftpy db:seed`: Seed the database
1592
+ - `swiftpy queue:work`: Start background queue worker
1593
+
1594
+ ## Docker Deployment
1595
+
1596
+ To spin up the entire application stack (App, PostgreSQL, and Redis):
1597
+ ```bash
1598
+ docker compose up -d
1599
+ ```
1600
+ """
1601
+
1602
+
1603
+ class NewCommand:
1604
+ """Console command for scaffolding fresh SwiftPY projects."""
1605
+
1606
+ @staticmethod
1607
+ def register(
1608
+ subparsers: argparse._SubParsersAction[argparse.ArgumentParser],
1609
+ ) -> None:
1610
+ parser = subparsers.add_parser(
1611
+ "new",
1612
+ help="Create a fresh SwiftPY project scaffolding",
1613
+ )
1614
+ parser.add_argument(
1615
+ "name",
1616
+ help="The name and directory of the new project",
1617
+ )
1618
+ parser.add_argument(
1619
+ "--force",
1620
+ action="store_true",
1621
+ default=False,
1622
+ help="Overwrite existing directory if not empty",
1623
+ )
1624
+ parser.set_defaults(
1625
+ _handler=NewCommand.handle,
1626
+ )
1627
+
1628
+ @classmethod
1629
+ def handle(cls, args: argparse.Namespace) -> None:
1630
+ raw_name: str = args.name.strip()
1631
+ if (
1632
+ not raw_name
1633
+ or "/" in raw_name
1634
+ or "\\" in raw_name
1635
+ or ".." in raw_name
1636
+ or not _NAME_PATTERN.fullmatch(raw_name)
1637
+ ):
1638
+ raise SystemExit(
1639
+ f"Invalid project name {raw_name!r}. Must contain only alphanumeric characters, underscores, or dashes."
1640
+ )
1641
+
1642
+ target_dir = Path.cwd() / raw_name
1643
+ if target_dir.is_symlink():
1644
+ raise SystemExit(
1645
+ f"Refusing to scaffold into symlinked project directory at {target_dir}"
1646
+ )
1647
+ resolved_base = Path.cwd().resolve()
1648
+ resolved_target = target_dir.resolve()
1649
+
1650
+ if (
1651
+ not resolved_target.is_relative_to(resolved_base)
1652
+ or resolved_target == resolved_base
1653
+ ):
1654
+ raise SystemExit(
1655
+ f"Invalid project path {raw_name!r}: cannot escape base directory or overwrite root."
1656
+ )
1657
+
1658
+ if (
1659
+ target_dir.exists()
1660
+ and any(target_dir.iterdir())
1661
+ and not getattr(args, "force", False)
1662
+ ):
1663
+ raise SystemExit(
1664
+ f"Directory '{target_dir}' already exists and is not empty. Use --force to overwrite."
1665
+ )
1666
+
1667
+ target_dir.mkdir(parents=True, exist_ok=True)
1668
+ if target_dir.is_symlink() or target_dir.resolve() != resolved_target:
1669
+ raise SystemExit(
1670
+ f"Invalid project path {raw_name!r}: project root resolves through a symlink or escapes base directory."
1671
+ )
1672
+
1673
+ # Generate cryptographically secure 32-byte APP_KEY encoded in base64
1674
+ raw_key = secrets.token_bytes(32)
1675
+ app_key = f"base64:{base64.b64encode(raw_key).decode('ascii')}"
1676
+
1677
+ # Create directory layout
1678
+ dirs_to_create = [
1679
+ "app/events",
1680
+ "app/http/handler",
1681
+ "app/http/middleware",
1682
+ "app/http/requests",
1683
+ "app/jobs",
1684
+ "app/listeners",
1685
+ "app/mail",
1686
+ "app/models",
1687
+ "app/providers",
1688
+ "app/repositories",
1689
+ "app/schemas",
1690
+ "app/services",
1691
+ "config",
1692
+ "database/migrations",
1693
+ "database/seeders",
1694
+ "public",
1695
+ "routes",
1696
+ "storage/app/private",
1697
+ "storage/app/public",
1698
+ "storage/framework",
1699
+ "storage/logs",
1700
+ "views/admin",
1701
+ "views/mail",
1702
+ "tests",
1703
+ ]
1704
+
1705
+ def _validate_safe_path(candidate: Path) -> None:
1706
+ current = candidate
1707
+ while True:
1708
+ if current.is_symlink():
1709
+ raise SystemExit(f"Refusing to traverse symlink at {current}")
1710
+ if current == target_dir:
1711
+ break
1712
+ if current.parent == current:
1713
+ break
1714
+ current = current.parent
1715
+ if not candidate.resolve().is_relative_to(resolved_target):
1716
+ raise SystemExit(
1717
+ f"Destination path escapes project directory: {candidate}"
1718
+ )
1719
+
1720
+ for d in dirs_to_create:
1721
+ dir_path = target_dir / d
1722
+ _validate_safe_path(dir_path)
1723
+ dir_path.mkdir(parents=True, exist_ok=True)
1724
+ _validate_safe_path(dir_path)
1725
+
1726
+ # Generate active .env with APP_KEY and JWT_SECRET
1727
+ jwt_secret = secrets.token_urlsafe(48)
1728
+ env_content = ENV_EXAMPLE_CONTENT.replace("APP_KEY=", f"APP_KEY={app_key}")
1729
+ env_content = env_content.replace("JWT_SECRET=", f"JWT_SECRET={jwt_secret}")
1730
+
1731
+ files: dict[str, str] = {
1732
+ ".dockerignore": DOCKERIGNORE_CONTENT,
1733
+ ".env.example": ENV_EXAMPLE_CONTENT,
1734
+ ".env": env_content,
1735
+ ".gitignore": GITIGNORE_CONTENT,
1736
+ "docker-compose.yml": DOCKER_COMPOSE_CONTENT,
1737
+ "Dockerfile": DOCKERFILE_CONTENT,
1738
+ "main.py": MAIN_PY_CONTENT,
1739
+ "pyproject.toml": _get_pyproject_toml(raw_name),
1740
+ "README.md": _get_readme(raw_name),
1741
+ "routes/__init__.py": "from __future__ import annotations\n",
1742
+ "routes/api.py": ROUTES_API_PY_CONTENT,
1743
+ "app/models/user.py": USER_MODEL_CONTENT,
1744
+ "app/schemas/user_schema.py": USER_SCHEMA_CONTENT,
1745
+ "app/services/user_service.py": USER_SERVICE_CONTENT,
1746
+ "app/repositories/user_repository.py": USER_REPOSITORY_CONTENT,
1747
+ "app/events/user_registered.py": USER_REGISTERED_EVENT_CONTENT,
1748
+ "app/listeners/send_welcome_notification.py": SEND_WELCOME_NOTIFICATION_CONTENT,
1749
+ "app/jobs/send_notification_job.py": SEND_NOTIFICATION_JOB_CONTENT,
1750
+ "app/mail/welcome_email.py": WELCOME_EMAIL_CONTENT,
1751
+ "app/http/handler/exceptions.py": EXCEPTIONS_CONTENT,
1752
+ "app/http/middleware/auth_middleware.py": AUTH_MIDDLEWARE_CONTENT,
1753
+ "app/http/middleware/admin_middleware.py": ADMIN_MIDDLEWARE_CONTENT,
1754
+ "app/http/requests/create_user_request.py": CREATE_USER_REQUEST_CONTENT,
1755
+ "app/providers/app_service_provider.py": APP_SERVICE_PROVIDER_CONTENT,
1756
+ "app/providers/auth_service_provider.py": AUTH_SERVICE_PROVIDER_CONTENT,
1757
+ "app/providers/cache_service_provider.py": CACHE_SERVICE_PROVIDER_CONTENT,
1758
+ "app/providers/database_service_provider.py": DATABASE_SERVICE_PROVIDER_CONTENT,
1759
+ "app/providers/event_service_provider.py": EVENT_SERVICE_PROVIDER_CONTENT,
1760
+ "app/providers/filesystem_service_provider.py": FILESYSTEM_SERVICE_PROVIDER_CONTENT,
1761
+ "app/providers/mail_service_provider.py": MAIL_SERVICE_PROVIDER_CONTENT,
1762
+ "app/providers/queue_service_provider.py": QUEUE_SERVICE_PROVIDER_CONTENT,
1763
+ "config/ai.py": CONFIG_AI_CONTENT,
1764
+ "config/app.py": CONFIG_APP_CONTENT,
1765
+ "config/auth.py": CONFIG_AUTH_CONTENT,
1766
+ "config/cache.py": CONFIG_CACHE_CONTENT,
1767
+ "config/cors.py": CONFIG_CORS_CONTENT,
1768
+ "config/database.py": CONFIG_DATABASE_CONTENT,
1769
+ "config/events.py": CONFIG_EVENTS_CONTENT,
1770
+ "config/filesystems.py": CONFIG_FILESYSTEMS_CONTENT,
1771
+ "config/logging.py": CONFIG_LOGGING_CONTENT,
1772
+ "config/mail.py": CONFIG_MAIL_CONTENT,
1773
+ "config/queue.py": CONFIG_QUEUE_CONTENT,
1774
+ "database/migrations/2026_01_01_000000_create_users_table.py": CREATE_USERS_MIGRATION_CONTENT,
1775
+ "database/seeders/database_seeder.py": DATABASE_SEEDER_CONTENT,
1776
+ "views/admin/dashboard.html": VIEWS_ADMIN_DASHBOARD_CONTENT,
1777
+ "views/admin/login.html": VIEWS_ADMIN_LOGIN_CONTENT,
1778
+ "views/mail/template.html": VIEWS_MAIL_TEMPLATE_CONTENT,
1779
+ "tests/conftest.py": TESTS_CONFTEST_CONTENT,
1780
+ "tests/test_api.py": TESTS_TEST_API_CONTENT,
1781
+ "storage/app/private/.gitkeep": "",
1782
+ "storage/app/public/.gitkeep": "",
1783
+ "storage/framework/.gitkeep": "",
1784
+ "storage/logs/.gitkeep": "",
1785
+ }
1786
+
1787
+ existing_env = (target_dir / ".env").exists()
1788
+
1789
+ for rel_path, content in files.items():
1790
+ dest = target_dir / rel_path
1791
+ # Preserve existing .env and secret keys when overwriting via --force
1792
+ if rel_path == ".env" and existing_env:
1793
+ continue
1794
+
1795
+ _validate_safe_path(dest)
1796
+ dest.parent.mkdir(parents=True, exist_ok=True)
1797
+ _validate_safe_path(dest)
1798
+
1799
+ dest.write_text(content, encoding="utf-8")
1800
+
1801
+ if sys.stdout.isatty() and "NO_COLOR" not in os.environ:
1802
+ print(
1803
+ f"\033[32mSuccessfully created SwiftPY application:\033[0m {raw_name}"
1804
+ )
1805
+ else:
1806
+ print(f"Successfully created SwiftPY application: {raw_name}")