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.
@@ -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
+ """