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,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>© {{ 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}")
|