stapel-tools 0.3.1__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.
- stapel_tools/__init__.py +1 -0
- stapel_tools/_compose_templates.py +373 -0
- stapel_tools/_library_templates.py +833 -0
- stapel_tools/_minimal_templates.py +144 -0
- stapel_tools/_templates.py +474 -0
- stapel_tools/create_project.py +718 -0
- stapel_tools/lint.py +412 -0
- stapel_tools/new_library.py +205 -0
- stapel_tools/new_module.py +123 -0
- stapel_tools/new_service.py +607 -0
- stapel_tools/remove_service.py +297 -0
- stapel_tools-0.3.1.dist-info/METADATA +112 -0
- stapel_tools-0.3.1.dist-info/RECORD +16 -0
- stapel_tools-0.3.1.dist-info/WHEEL +5 -0
- stapel_tools-0.3.1.dist-info/entry_points.txt +7 -0
- stapel_tools-0.3.1.dist-info/top_level.txt +1 -0
stapel_tools/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"""Docker Compose templates for project scaffolding."""
|
|
2
|
+
|
|
3
|
+
# Runs on EVERY postgres startup (via the db service's command wrapper),
|
|
4
|
+
# creating any database listed in POSTGRES_MULTIPLE_DATABASES that does not
|
|
5
|
+
# exist yet. Running at every startup — not just first initdb — means adding
|
|
6
|
+
# a service later creates its database without wiping the data volume.
|
|
7
|
+
POSTGRES_ENSURE_DATABASES = """\
|
|
8
|
+
#!/bin/sh
|
|
9
|
+
set -eu
|
|
10
|
+
|
|
11
|
+
ensure_database_exists() {
|
|
12
|
+
database=$1
|
|
13
|
+
if psql -v ON_ERROR_STOP=1 -d postgres --username "$POSTGRES_USER" -lqt \\
|
|
14
|
+
| cut -d '|' -f 1 | grep -qw "$database"; then
|
|
15
|
+
echo " database '$database' exists"
|
|
16
|
+
else
|
|
17
|
+
echo " creating database '$database'"
|
|
18
|
+
psql -v ON_ERROR_STOP=1 -d postgres --username "$POSTGRES_USER" <<-EOSQL
|
|
19
|
+
CREATE DATABASE $database;
|
|
20
|
+
GRANT ALL PRIVILEGES ON DATABASE $database TO $POSTGRES_USER;
|
|
21
|
+
EOSQL
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if [ -n "${POSTGRES_MULTIPLE_DATABASES:-}" ]; then
|
|
26
|
+
echo "Ensuring databases exist: $POSTGRES_MULTIPLE_DATABASES"
|
|
27
|
+
for db in $(echo "$POSTGRES_MULTIPLE_DATABASES" | tr ',' ' '); do
|
|
28
|
+
db=$(echo "$db" | xargs)
|
|
29
|
+
[ -n "$db" ] && ensure_database_exists "$db"
|
|
30
|
+
done
|
|
31
|
+
fi
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Mounted at /etc/nginx/conf.d/nginx.conf. stapel-new-service appends a
|
|
35
|
+
# location block per service before the closing brace.
|
|
36
|
+
NGINX_CONF = """\
|
|
37
|
+
server {
|
|
38
|
+
listen 80;
|
|
39
|
+
server_name _;
|
|
40
|
+
client_max_body_size 50m;
|
|
41
|
+
resolver 127.0.0.11 valid=10s;
|
|
42
|
+
|
|
43
|
+
location /staticfiles/ {
|
|
44
|
+
alias /staticfiles/;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
location /media/ {
|
|
48
|
+
alias /media/;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ─── Broker building blocks ─────────────────────────────────────────────────
|
|
55
|
+
# Compose bases carry {{BROKER_SERVICES}} / {{BROKER_VOLUMES}} markers; the
|
|
56
|
+
# generator splices the chosen broker(s) in. Env templates carry
|
|
57
|
+
# {{BROKER_ENV}}. A dedicated Task broker (--task-broker) adds its blocks
|
|
58
|
+
# next to the event broker's.
|
|
59
|
+
|
|
60
|
+
NATS_SERVICE_BLOCK = """\
|
|
61
|
+
# Events (JetStream) + RPC (request-reply) for stapel_core.comm
|
|
62
|
+
nats:
|
|
63
|
+
image: nats:2.10-alpine
|
|
64
|
+
restart: unless-stopped
|
|
65
|
+
command: ["--jetstream", "--store_dir", "/data"]
|
|
66
|
+
volumes:
|
|
67
|
+
- nats-data:/data
|
|
68
|
+
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
KAFKA_SERVICE_BLOCK = """\
|
|
72
|
+
kafka:
|
|
73
|
+
image: apache/kafka:3.9.0
|
|
74
|
+
restart: unless-stopped
|
|
75
|
+
environment:
|
|
76
|
+
KAFKA_NODE_ID: 1
|
|
77
|
+
KAFKA_PROCESS_ROLES: broker,controller
|
|
78
|
+
KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093
|
|
79
|
+
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
|
|
80
|
+
KAFKA_CONTROLLER_QUORUM_VOTERS: 1@kafka:9093
|
|
81
|
+
KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
|
|
82
|
+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
|
83
|
+
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
|
84
|
+
volumes:
|
|
85
|
+
- kafka-data:/var/lib/kafka/data
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
NATS_ENV_BLOCK = """\
|
|
90
|
+
# ─── NATS: events (JetStream) + RPC ─────────────────────────────────────────
|
|
91
|
+
STAPEL_BUS_BACKEND=nats
|
|
92
|
+
NATS_URL=nats://nats:4222
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
KAFKA_ENV_BLOCK = """\
|
|
96
|
+
# ─── Kafka: events ──────────────────────────────────────────────────────────
|
|
97
|
+
STAPEL_BUS_BACKEND=kafka
|
|
98
|
+
KAFKA_BOOTSTRAP_SERVERS=kafka:9092
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
# Task broker only (monolith --task-broker nats): Actions stay in-process;
|
|
102
|
+
# STAPEL_TASK_DISPATCH=bus makes stapel_core publish task.* events through
|
|
103
|
+
# the broker to a dedicated worker (STAPEL_COMM["TASK_DISPATCH"]).
|
|
104
|
+
TASK_ONLY_NATS_ENV_BLOCK = """\
|
|
105
|
+
# ─── NATS: broker for long-running Tasks only (Actions stay in-process) ────
|
|
106
|
+
STAPEL_BUS_BACKEND=nats
|
|
107
|
+
NATS_URL=nats://nats:4222
|
|
108
|
+
STAPEL_TASK_DISPATCH=bus
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
_BROKER_SERVICES = {"nats": NATS_SERVICE_BLOCK, "kafka": KAFKA_SERVICE_BLOCK, "none": ""}
|
|
112
|
+
_BROKER_VOLUMES = {"nats": " nats-data:\n", "kafka": " kafka-data:\n", "none": ""}
|
|
113
|
+
_BROKER_ENV = {"nats": NATS_ENV_BLOCK, "kafka": KAFKA_ENV_BLOCK, "none": ""}
|
|
114
|
+
_BROKER_URL_LINES = {
|
|
115
|
+
"nats": "NATS_URL=nats://nats:4222\n",
|
|
116
|
+
"kafka": "KAFKA_BOOTSTRAP_SERVERS=kafka:9092\n",
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def render_compose_base(template: str, broker: str, task_broker: str = "none") -> str:
|
|
121
|
+
"""Splice the chosen broker(s) (nats | kafka | none) into a compose base.
|
|
122
|
+
|
|
123
|
+
*task_broker* adds a second broker dedicated to Tasks when it differs
|
|
124
|
+
from the event broker.
|
|
125
|
+
"""
|
|
126
|
+
brokers = [b for b in ("nats", "kafka") if b in (broker, task_broker)]
|
|
127
|
+
services = "".join(_BROKER_SERVICES[b] for b in brokers)
|
|
128
|
+
volumes = "".join(_BROKER_VOLUMES[b] for b in brokers)
|
|
129
|
+
return template.replace("{{BROKER_SERVICES}}", services).replace(
|
|
130
|
+
"{{BROKER_VOLUMES}}", volumes
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _broker_env(broker: str, task_broker: str) -> str:
|
|
135
|
+
if task_broker in ("none", broker):
|
|
136
|
+
return _BROKER_ENV[broker]
|
|
137
|
+
if broker == "none":
|
|
138
|
+
# Broker exists for Tasks only — Actions stay in-process.
|
|
139
|
+
return TASK_ONLY_NATS_ENV_BLOCK
|
|
140
|
+
# Two brokers: RoutingBus splits by topic prefix — task.* to the task
|
|
141
|
+
# broker, everything else to the event broker.
|
|
142
|
+
urls = "".join(
|
|
143
|
+
_BROKER_URL_LINES[b] for b in ("nats", "kafka") if b in (broker, task_broker)
|
|
144
|
+
)
|
|
145
|
+
return (
|
|
146
|
+
f"# ─── Brokers: {broker} for events/RPC, {task_broker} for Tasks ─────────────────\n"
|
|
147
|
+
"STAPEL_BUS_BACKEND=routing\n"
|
|
148
|
+
f'STAPEL_BUS_ROUTES={{"task.": "{task_broker}", "": "{broker}"}}\n'
|
|
149
|
+
f"{urls}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def render_env(template: str, broker: str, ctx: dict, task_broker: str = "none") -> str:
|
|
154
|
+
# Format first ({{BROKER_ENV}} collapses to {BROKER_ENV}), then splice the
|
|
155
|
+
# broker block in — it may contain literal braces (STAPEL_BUS_ROUTES JSON)
|
|
156
|
+
# that str.format must never see.
|
|
157
|
+
rendered = template.format(**ctx)
|
|
158
|
+
return rendered.replace("{BROKER_ENV}", _broker_env(broker, task_broker))
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
MONOLITH_COMPOSE_BASE = """\
|
|
162
|
+
# Shared infrastructure — included by all environments.
|
|
163
|
+
# Do not put service-specific overrides here.
|
|
164
|
+
services:
|
|
165
|
+
db:
|
|
166
|
+
image: postgres:16
|
|
167
|
+
restart: unless-stopped
|
|
168
|
+
environment:
|
|
169
|
+
POSTGRES_USER: "${POSTGRES_USER:-stapel}"
|
|
170
|
+
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
|
|
171
|
+
POSTGRES_MULTIPLE_DATABASES: "stapel_main"
|
|
172
|
+
volumes:
|
|
173
|
+
- db-data:/var/lib/postgresql/data
|
|
174
|
+
- ./service-configs/postgres/ensure-databases.sh:/usr/local/bin/ensure-databases.sh:ro
|
|
175
|
+
command: >
|
|
176
|
+
bash -c "
|
|
177
|
+
docker-entrypoint.sh postgres &
|
|
178
|
+
until pg_isready -U $${POSTGRES_USER:-stapel}; do sleep 1; done
|
|
179
|
+
/usr/local/bin/ensure-databases.sh
|
|
180
|
+
wait
|
|
181
|
+
"
|
|
182
|
+
healthcheck:
|
|
183
|
+
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-stapel}"]
|
|
184
|
+
interval: 5s
|
|
185
|
+
timeout: 5s
|
|
186
|
+
retries: 10
|
|
187
|
+
|
|
188
|
+
redis:
|
|
189
|
+
image: redis:7-alpine
|
|
190
|
+
restart: unless-stopped
|
|
191
|
+
volumes:
|
|
192
|
+
- redis-data:/data
|
|
193
|
+
|
|
194
|
+
{{BROKER_SERVICES}} nginx:
|
|
195
|
+
image: nginx:alpine
|
|
196
|
+
restart: unless-stopped
|
|
197
|
+
ports:
|
|
198
|
+
- "${HTTP_PORT:-80}:80"
|
|
199
|
+
volumes:
|
|
200
|
+
- ./service-configs/nginx:/etc/nginx/conf.d:ro
|
|
201
|
+
- static-content:/staticfiles:ro
|
|
202
|
+
- media-content:/media:ro
|
|
203
|
+
depends_on: []
|
|
204
|
+
|
|
205
|
+
volumes:
|
|
206
|
+
db-data:
|
|
207
|
+
redis-data:
|
|
208
|
+
{{BROKER_VOLUMES}} static-content:
|
|
209
|
+
media-content:
|
|
210
|
+
"""
|
|
211
|
+
|
|
212
|
+
MONOLITH_COMPOSE_LOCAL = """\
|
|
213
|
+
include:
|
|
214
|
+
- docker-compose.base.yml
|
|
215
|
+
|
|
216
|
+
services:
|
|
217
|
+
# Add services from their individual .yml files:
|
|
218
|
+
# svc-app:
|
|
219
|
+
# extends:
|
|
220
|
+
# file: svc-app.yml
|
|
221
|
+
# service: svc-app
|
|
222
|
+
# volumes:
|
|
223
|
+
# - ./svc-app:/app
|
|
224
|
+
# - ./stapel_core:/app/stapel_core:ro
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
MONOLITH_COMPOSE_DEV = """\
|
|
228
|
+
include:
|
|
229
|
+
- docker-compose.base.yml
|
|
230
|
+
|
|
231
|
+
services:
|
|
232
|
+
# Add services from their individual .yml files:
|
|
233
|
+
# svc-app:
|
|
234
|
+
# extends:
|
|
235
|
+
# file: svc-app.yml
|
|
236
|
+
# service: svc-app
|
|
237
|
+
"""
|
|
238
|
+
|
|
239
|
+
MONOLITH_ENV_TEMPLATE = """\
|
|
240
|
+
# ─── Database ──────────────────────────────────────────────────────────────
|
|
241
|
+
POSTGRES_USER=stapel
|
|
242
|
+
POSTGRES_PASSWORD=change_me
|
|
243
|
+
POSTGRES_HOST=db
|
|
244
|
+
POSTGRES_PORT=5432
|
|
245
|
+
|
|
246
|
+
# ─── Redis ─────────────────────────────────────────────────────────────────
|
|
247
|
+
REDIS_URL=redis://redis:6379/0
|
|
248
|
+
|
|
249
|
+
{{BROKER_ENV}}
|
|
250
|
+
# ─── App ───────────────────────────────────────────────────────────────────
|
|
251
|
+
SECRET_KEY=change_me_to_a_long_random_string
|
|
252
|
+
JWT_SECRET_KEY=change_me_to_another_long_random_string
|
|
253
|
+
ALLOWED_HOSTS={domain}
|
|
254
|
+
SITE_URL={url}
|
|
255
|
+
|
|
256
|
+
# ─── Email ─────────────────────────────────────────────────────────────────
|
|
257
|
+
DEFAULT_FROM_EMAIL={company_name} <{company_email}>
|
|
258
|
+
EMAIL_HOST=smtp.example.com
|
|
259
|
+
EMAIL_PORT=587
|
|
260
|
+
EMAIL_HOST_USER=
|
|
261
|
+
EMAIL_HOST_PASSWORD=
|
|
262
|
+
|
|
263
|
+
# ─── OAuth (optional) ───────────────────────────────────────────────────────
|
|
264
|
+
GOOGLE_OAUTH2_KEY=
|
|
265
|
+
GOOGLE_OAUTH2_SECRET=
|
|
266
|
+
|
|
267
|
+
# ─── Run command ────────────────────────────────────────────────────────────
|
|
268
|
+
RUN_CMD=gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 2
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
MONOLITH_GITIGNORE = """\
|
|
272
|
+
.env
|
|
273
|
+
*.pyc
|
|
274
|
+
__pycache__/
|
|
275
|
+
.venv/
|
|
276
|
+
venv/
|
|
277
|
+
*.egg-info/
|
|
278
|
+
dist/
|
|
279
|
+
build/
|
|
280
|
+
htmlcov/
|
|
281
|
+
.coverage
|
|
282
|
+
*.sqlite3
|
|
283
|
+
media/
|
|
284
|
+
staticfiles/
|
|
285
|
+
.DS_Store
|
|
286
|
+
"""
|
|
287
|
+
|
|
288
|
+
MICRO_COMPOSE_BASE = """\
|
|
289
|
+
# Shared infrastructure for microservices stack.
|
|
290
|
+
services:
|
|
291
|
+
db:
|
|
292
|
+
image: postgres:16
|
|
293
|
+
restart: unless-stopped
|
|
294
|
+
environment:
|
|
295
|
+
POSTGRES_USER: "${POSTGRES_USER:-stapel}"
|
|
296
|
+
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}"
|
|
297
|
+
POSTGRES_MULTIPLE_DATABASES: ""
|
|
298
|
+
volumes:
|
|
299
|
+
- db-data:/var/lib/postgresql/data
|
|
300
|
+
- ./service-configs/postgres/ensure-databases.sh:/usr/local/bin/ensure-databases.sh:ro
|
|
301
|
+
command: >
|
|
302
|
+
bash -c "
|
|
303
|
+
docker-entrypoint.sh postgres &
|
|
304
|
+
until pg_isready -U $${POSTGRES_USER:-stapel}; do sleep 1; done
|
|
305
|
+
/usr/local/bin/ensure-databases.sh
|
|
306
|
+
wait
|
|
307
|
+
"
|
|
308
|
+
healthcheck:
|
|
309
|
+
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER:-stapel}"]
|
|
310
|
+
interval: 5s
|
|
311
|
+
timeout: 5s
|
|
312
|
+
retries: 10
|
|
313
|
+
|
|
314
|
+
redis:
|
|
315
|
+
image: redis:7-alpine
|
|
316
|
+
restart: unless-stopped
|
|
317
|
+
volumes:
|
|
318
|
+
- redis-data:/data
|
|
319
|
+
|
|
320
|
+
{{BROKER_SERVICES}} nginx:
|
|
321
|
+
image: nginx:alpine
|
|
322
|
+
restart: unless-stopped
|
|
323
|
+
ports:
|
|
324
|
+
- "${HTTP_PORT:-80}:80"
|
|
325
|
+
volumes:
|
|
326
|
+
- ./service-configs/nginx:/etc/nginx/conf.d:ro
|
|
327
|
+
- static-content:/staticfiles:ro
|
|
328
|
+
- media-content:/media:ro
|
|
329
|
+
depends_on: []
|
|
330
|
+
|
|
331
|
+
volumes:
|
|
332
|
+
db-data:
|
|
333
|
+
redis-data:
|
|
334
|
+
{{BROKER_VOLUMES}} static-content:
|
|
335
|
+
media-content:
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
MICRO_COMPOSE_LOCAL = """\
|
|
339
|
+
include:
|
|
340
|
+
- docker-compose.base.yml
|
|
341
|
+
|
|
342
|
+
services:
|
|
343
|
+
# Add your services here.
|
|
344
|
+
# Run: stapel-new-service <name> --prefix svc-
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
MICRO_ENV_TEMPLATE = """\
|
|
348
|
+
# ─── Database ──────────────────────────────────────────────────────────────
|
|
349
|
+
POSTGRES_USER=stapel
|
|
350
|
+
POSTGRES_PASSWORD=change_me
|
|
351
|
+
POSTGRES_HOST=db
|
|
352
|
+
POSTGRES_PORT=5432
|
|
353
|
+
|
|
354
|
+
# ─── Redis ─────────────────────────────────────────────────────────────────
|
|
355
|
+
REDIS_URL=redis://redis:6379/0
|
|
356
|
+
|
|
357
|
+
{{BROKER_ENV}}
|
|
358
|
+
# ─── App ───────────────────────────────────────────────────────────────────
|
|
359
|
+
SECRET_KEY=change_me_to_a_long_random_string
|
|
360
|
+
JWT_SECRET_KEY=change_me_to_another_long_random_string
|
|
361
|
+
ALLOWED_HOSTS={domain}
|
|
362
|
+
SITE_URL={url}
|
|
363
|
+
|
|
364
|
+
# ─── Email ─────────────────────────────────────────────────────────────────
|
|
365
|
+
DEFAULT_FROM_EMAIL={company_name} <{company_email}>
|
|
366
|
+
EMAIL_HOST=smtp.example.com
|
|
367
|
+
EMAIL_PORT=587
|
|
368
|
+
EMAIL_HOST_USER=
|
|
369
|
+
EMAIL_HOST_PASSWORD=
|
|
370
|
+
|
|
371
|
+
# ─── Run command ────────────────────────────────────────────────────────────
|
|
372
|
+
RUN_CMD=gunicorn core.wsgi:application --bind 0.0.0.0:8000 --workers 2
|
|
373
|
+
"""
|