cupli 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. cupli-0.1.1/.gitignore +48 -0
  2. cupli-0.1.1/AGENTS.md +385 -0
  3. cupli-0.1.1/LICENSE +21 -0
  4. cupli-0.1.1/Makefile +113 -0
  5. cupli-0.1.1/PKG-INFO +906 -0
  6. cupli-0.1.1/README.md +747 -0
  7. cupli-0.1.1/pyproject.toml +252 -0
  8. cupli-0.1.1/src/cupli/__init__.py +1 -0
  9. cupli-0.1.1/src/cupli/__main__.py +6 -0
  10. cupli-0.1.1/src/cupli/cli/__init__.py +7 -0
  11. cupli-0.1.1/src/cupli/cli/_completion.py +157 -0
  12. cupli-0.1.1/src/cupli/cli/container.py +28 -0
  13. cupli-0.1.1/src/cupli/cli/dashboard.py +106 -0
  14. cupli-0.1.1/src/cupli/cli/diagnostics.py +74 -0
  15. cupli-0.1.1/src/cupli/cli/exec.py +364 -0
  16. cupli-0.1.1/src/cupli/cli/git.py +189 -0
  17. cupli-0.1.1/src/cupli/cli/hooks.py +133 -0
  18. cupli-0.1.1/src/cupli/cli/lifecycle.py +232 -0
  19. cupli-0.1.1/src/cupli/cli/mounts.py +114 -0
  20. cupli-0.1.1/src/cupli/cli/root.py +395 -0
  21. cupli-0.1.1/src/cupli/cli/workspace.py +459 -0
  22. cupli-0.1.1/src/cupli/core/__init__.py +1 -0
  23. cupli-0.1.1/src/cupli/core/c3.py +116 -0
  24. cupli-0.1.1/src/cupli/core/cache.py +149 -0
  25. cupli-0.1.1/src/cupli/core/env_resolver.py +189 -0
  26. cupli-0.1.1/src/cupli/core/loader.py +461 -0
  27. cupli-0.1.1/src/cupli/core/parser.py +159 -0
  28. cupli-0.1.1/src/cupli/core/registry.py +277 -0
  29. cupli-0.1.1/src/cupli/domain/__init__.py +1 -0
  30. cupli-0.1.1/src/cupli/domain/consts.py +137 -0
  31. cupli-0.1.1/src/cupli/domain/enums.py +58 -0
  32. cupli-0.1.1/src/cupli/domain/errors.py +285 -0
  33. cupli-0.1.1/src/cupli/domain/models.py +477 -0
  34. cupli-0.1.1/src/cupli/domain/plan.py +92 -0
  35. cupli-0.1.1/src/cupli/domain/runtime.py +42 -0
  36. cupli-0.1.1/src/cupli/manage.py +10 -0
  37. cupli-0.1.1/src/cupli/services/__init__.py +1 -0
  38. cupli-0.1.1/src/cupli/services/compose_service.py +745 -0
  39. cupli-0.1.1/src/cupli/services/filter_service.py +140 -0
  40. cupli-0.1.1/src/cupli/services/git_service.py +366 -0
  41. cupli-0.1.1/src/cupli/services/hooks_service.py +472 -0
  42. cupli-0.1.1/src/cupli/services/ide_setup_service.py +233 -0
  43. cupli-0.1.1/src/cupli/services/mounts_service.py +122 -0
  44. cupli-0.1.1/src/cupli/services/workspace_service.py +329 -0
  45. cupli-0.1.1/src/cupli/utils/__init__.py +1 -0
  46. cupli-0.1.1/src/cupli/utils/console.py +161 -0
  47. cupli-0.1.1/src/cupli/utils/exceptions.py +90 -0
  48. cupli-0.1.1/src/cupli/utils/fuzzy.py +39 -0
  49. cupli-0.1.1/src/cupli/utils/git.py +141 -0
  50. cupli-0.1.1/src/cupli/utils/json.py +14 -0
  51. cupli-0.1.1/src/cupli/utils/lock.py +101 -0
  52. cupli-0.1.1/src/cupli/utils/path.py +85 -0
  53. cupli-0.1.1/src/cupli/utils/subprocess.py +74 -0
  54. cupli-0.1.1/src/cupli/version.py +42 -0
  55. cupli-0.1.1/tests/__init__.py +0 -0
  56. cupli-0.1.1/tests/cli/__init__.py +0 -0
  57. cupli-0.1.1/tests/cli/test_exec.py +233 -0
  58. cupli-0.1.1/tests/cli/test_hooks.py +73 -0
  59. cupli-0.1.1/tests/cli/test_mounts.py +80 -0
  60. cupli-0.1.1/tests/cli/test_root.py +82 -0
  61. cupli-0.1.1/tests/cli/test_workspace.py +265 -0
  62. cupli-0.1.1/tests/conftest.py +37 -0
  63. cupli-0.1.1/tests/fixtures/spaces/invalid/bad_name.yaml +3 -0
  64. cupli-0.1.1/tests/fixtures/spaces/invalid/bad_version.yaml +4 -0
  65. cupli-0.1.1/tests/fixtures/spaces/invalid/bad_yaml_syntax.yaml +4 -0
  66. cupli-0.1.1/tests/fixtures/spaces/invalid/command_unknown_container.yaml +7 -0
  67. cupli-0.1.1/tests/fixtures/spaces/invalid/empty.yaml +1 -0
  68. cupli-0.1.1/tests/fixtures/spaces/invalid/missing_apps.yaml +3 -0
  69. cupli-0.1.1/tests/fixtures/spaces/invalid/mount_relative_exec_path.yaml +7 -0
  70. cupli-0.1.1/tests/fixtures/spaces/invalid/mount_unknown_host.yaml +7 -0
  71. cupli-0.1.1/tests/fixtures/spaces/invalid/unknown_base.yaml +4 -0
  72. cupli-0.1.1/tests/fixtures/spaces/invalid/unknown_dep.yaml +5 -0
  73. cupli-0.1.1/tests/fixtures/spaces/minimal/space.cupli.yaml +4 -0
  74. cupli-0.1.1/tests/fixtures/spaces/with_bases/space.cupli.yaml +22 -0
  75. cupli-0.1.1/tests/fixtures/spaces/with_mounts/space.cupli.yaml +22 -0
  76. cupli-0.1.1/tests/integration/__init__.py +0 -0
  77. cupli-0.1.1/tests/integration/test_lifecycle.py +123 -0
  78. cupli-0.1.1/tests/unit/__init__.py +0 -0
  79. cupli-0.1.1/tests/unit/test_auto_register.py +67 -0
  80. cupli-0.1.1/tests/unit/test_c3.py +50 -0
  81. cupli-0.1.1/tests/unit/test_compose_service.py +770 -0
  82. cupli-0.1.1/tests/unit/test_env_resolver.py +193 -0
  83. cupli-0.1.1/tests/unit/test_excepthook.py +62 -0
  84. cupli-0.1.1/tests/unit/test_exceptions.py +64 -0
  85. cupli-0.1.1/tests/unit/test_filter_service.py +77 -0
  86. cupli-0.1.1/tests/unit/test_fuzzy.py +27 -0
  87. cupli-0.1.1/tests/unit/test_git_service.py +131 -0
  88. cupli-0.1.1/tests/unit/test_hooks_service.py +141 -0
  89. cupli-0.1.1/tests/unit/test_ide_setup_service.py +123 -0
  90. cupli-0.1.1/tests/unit/test_loader.py +197 -0
  91. cupli-0.1.1/tests/unit/test_lock.py +47 -0
  92. cupli-0.1.1/tests/unit/test_models_coercion.py +67 -0
  93. cupli-0.1.1/tests/unit/test_mounts_service.py +75 -0
  94. cupli-0.1.1/tests/unit/test_parser.py +165 -0
  95. cupli-0.1.1/tests/unit/test_registry.py +263 -0
  96. cupli-0.1.1/tests/unit/test_workspace_service.py +261 -0
cupli-0.1.1/.gitignore ADDED
@@ -0,0 +1,48 @@
1
+ # .gitignore, adopted from pydantic .gitignore (https://github.com/pydantic/pydantic/blob/main/.gitignore).
2
+
3
+ # Virtual environments
4
+ env/
5
+ env3*/
6
+ venv/
7
+ .venv/
8
+ __pypackages__/
9
+
10
+ # IDEs and editors
11
+ .idea/
12
+ .vscode/
13
+
14
+ # Package distribution and build files
15
+ *.egg-info/
16
+ dist/
17
+ /build/
18
+ _build/
19
+
20
+ # Python bytecode and cache files
21
+ *.py[cod]
22
+ .cache/
23
+ /.ghtopdep_cache/
24
+ .hypothesis
25
+ .pytest_cache/
26
+ /.ruff_cache/
27
+
28
+ # Test files
29
+ /htmlcov/
30
+ /codecov.sh
31
+ /coverage.lcov
32
+ .coverage
33
+ /.env.test.local
34
+
35
+ # Documentation files
36
+ /docs/changelog.md
37
+ /docs/*.whl
38
+ /docs/theme/mkdocs_run_deps.html
39
+ /site/
40
+ /site.zip
41
+
42
+ # Other files and folders
43
+ .python-version
44
+ .DS_Store
45
+ .auto-format
46
+ /sandbox/
47
+ /worktrees/
48
+ /pyrightconfig.json
cupli-0.1.1/AGENTS.md ADDED
@@ -0,0 +1,385 @@
1
+ # AGENTS.md — instructions for LLM agents working in a cupli workspace
2
+
3
+ This document is for AI agents (Claude Code, Cursor, GitHub Copilot,
4
+ Codex, Aider) that need to **read and edit `space.cupli.yaml`** in a
5
+ project that uses cupli. Read this once, then operate confidently.
6
+
7
+ > If you're an agent modifying **cupli's own source code**, that's a
8
+ > different document. See [`README.md`](README.md) Contributing section.
9
+
10
+ ---
11
+
12
+ ## What cupli is
13
+
14
+ A CLI that turns one `space.cupli.yaml` into one running
15
+ docker-compose project. It manages:
16
+
17
+ 1. **Multi-repo workspaces** — each app/mount may have `repo:` + `branch:`.
18
+ Cupli clones them under `src/apps/<name>` etc.
19
+ 2. **Service binding** — every app binds to one or more compose-services.
20
+ Forms: implicit, string, inline dict, multi-service map (see below).
21
+ 3. **Variable scope** — space → bases (C3) → app, with `${VAR}` and
22
+ `${VAR:-default}` interpolation.
23
+ 4. **Lifecycle** — `cupli up/down/restart/logs/ps/build/pull`,
24
+ `cupli exec/run/shell`, `cupli git status/pull/fetch/checkout`,
25
+ `cupli mounts attach/detach`, `cupli sc <name>`.
26
+ 5. **Validation** — every YAML is validated against a Pydantic schema.
27
+ Failures surface with `file:line:col` per field.
28
+
29
+ The authoritative schema is in [`space.schema.json`](space.schema.json).
30
+ Every example YAML carries a `# yaml-language-server: $schema=...`
31
+ directive on line 1.
32
+
33
+ ---
34
+
35
+ ## The mental model in 30 seconds
36
+
37
+ ```
38
+ space.cupli.yaml
39
+ ├─ name: project + network name
40
+ ├─ vars: shared across the whole stack
41
+ ├─ bases: reusable templates (vars + composes + ...)
42
+ ├─ apps: what cupli starts/stops
43
+ │ └─ each app binds to compose-services via:
44
+ │ • implicit: service name = app name
45
+ │ • service: name → rename binding to existing compose service
46
+ │ • service: {...} → inline single-service (any compose attrs)
47
+ │ • services: {...}→ multi-service map (compound app)
48
+ │ • services: [a,b]→ same, list-form shorthand for empty overrides
49
+ ├─ mounts: toggleable bind-mounts (cupli mounts attach/detach)
50
+ ├─ networks: docker-compose `networks:` block (compose-spec verbatim)
51
+ └─ commands: shortcuts (cupli sc <name> / cupli <name>)
52
+ ```
53
+
54
+ Cupli writes three generated files under `.locals/<space>/state/` on
55
+ every invocation:
56
+
57
+ * `docker-compose.pre.yml` — defaults (network, `container_name`).
58
+ * `docker-compose.inline.yml` — services declared inline.
59
+ * `docker-compose.post.yml` — `environment` / `ports` / `volumes` /
60
+ `depends_on` / `networks` injection.
61
+
62
+ Plus `override.env` with all space-scope vars + per-component
63
+ `<NAME>_APP_PATH` etc. that docker-compose substitutes into compose
64
+ files.
65
+
66
+ ---
67
+
68
+ ## Quick rules — what you must / must not do
69
+
70
+ ### MUST
71
+
72
+ * **Keep schema_version: 1.** Don't change it.
73
+ * **Match `name:` to `^[A-Za-z][A-Za-z0-9_-]*$`.**
74
+ * **Make `service:` and `services:` mutually exclusive.** Pydantic
75
+ raises `E002` otherwise.
76
+ * **Inside `service: {...}` or `services.<n>: {...}`, write any
77
+ docker-compose service attribute verbatim** (`image`, `build`,
78
+ `command`, `environment`, `depends_on`, `healthcheck`, `volumes`,
79
+ `restart`, `ulimits`, `cap_add`, etc.). Cupli only intercepts
80
+ `vars` and `ports`.
81
+ * **Use `${VAR}` and `${VAR:-default}` for interpolation.** Bareword
82
+ `$VAR` is not recognised.
83
+ * **Use auto-vars when referring to paths:**
84
+ * `${SPACE_PATH}`, `${APPS_PATH}`, `${BASES_PATH}`, `${MOUNTS_PATH}`,
85
+ `${LOCALS_PATH}` — space scope.
86
+ * `${APP_PATH}`, `${APP_NAME}`, `${APP_LOCAL_PATH}` — self-reference
87
+ inside the current app / base / mount (cupli stores every
88
+ component's path as `APP_PATH` regardless of kind — the name is a
89
+ leftover from the apps-only era).
90
+ * `${MOUNT_PATH}`, `${MOUNT_EXEC_PATH}` — inside a mount.
91
+ * `${<NAME>_APP_PATH}`, `${<NAME>_BASE_PATH}`, `${<NAME>_MOUNT_PATH}`
92
+ — cross-component (`<NAME>` is the component name upper-cased with
93
+ `-` → `_`).
94
+ * **Inline compose-spec is substituted by docker-compose, not cupli.**
95
+ Inside `service: {build: {context: ${API_APP_PATH}}}` use the
96
+ per-component path-var (`${API_APP_PATH}`), NOT bare `${APP_PATH}`.
97
+ Cupli writes `<NAME>_APP_PATH` into `override.env` so compose can
98
+ resolve them; bare `APP_PATH` is only set in cupli's own scope (used
99
+ when cupli itself does the substitution, e.g. in `composes:` lists).
100
+ * **Run `cupli space doctor` after edits** to catch validation errors
101
+ with line numbers.
102
+
103
+ ### MUST NOT
104
+
105
+ * **Don't put `container_name`, `networks`, or `depends_on`-from-cupli's-`deps:`
106
+ into compose files manually.** Cupli generates them. Yours will get
107
+ merged but is redundant.
108
+ * **Don't hardcode absolute paths.** Use `${APPS_PATH}/<name>` or
109
+ `${<NAME>_APP_PATH}` instead.
110
+ * **Don't put `<NAME>_APP_PATH` into space's `vars:` manually.** Cupli
111
+ generates these automatically for every declared component.
112
+ * **Don't edit files under `.locals/<space>/state/`.** They are
113
+ regenerated and carry an `# AUTO-GENERATED by cupli. DO NOT EDIT`
114
+ banner.
115
+ * **Don't put secrets in `vars:`.** They land in `override.env` which
116
+ may be committed. Use `envs: [./.env.local]` and gitignore the file.
117
+ * **Don't change `mode: oneshot` to `mode: up` for migration / seeder
118
+ tasks.** It changes the lifecycle semantics — `up` containers keep
119
+ running, `oneshot` ones exit and block dependents on
120
+ `service_completed_successfully`.
121
+
122
+ ---
123
+
124
+ ## Service binding cheat-sheet
125
+
126
+ ### Form 1 — implicit (compose service name = app name)
127
+
128
+ ```yaml
129
+ apps:
130
+ postgres:
131
+ composes: [ ./infra/postgres.compose.yml ]
132
+ ```
133
+
134
+ Cupli binds to `services.postgres` in the compose file. Use when names
135
+ already match.
136
+
137
+ ### Form 2 — string rename
138
+
139
+ ```yaml
140
+ apps:
141
+ redis:
142
+ service: agora-redis # compose service is named differently
143
+ composes: [ ./compose.yml ]
144
+ ```
145
+
146
+ Use when an existing compose-fragment ships a service whose name
147
+ doesn't equal your cupli app name.
148
+
149
+ ### Form 3 — inline single-service (no compose file)
150
+
151
+ ```yaml
152
+ apps:
153
+ cache:
154
+ service:
155
+ image: redis:7-alpine
156
+ command: [ "redis-server", "--appendonly", "yes" ]
157
+ healthcheck:
158
+ test: [ "CMD", "redis-cli", "ping" ]
159
+ interval: 5s
160
+ restart: unless-stopped
161
+ vars:
162
+ LOG_LEVEL: info # injected as services.cache.environment
163
+ ports:
164
+ - "6379:6379" # injected as services.cache.ports
165
+ ```
166
+
167
+ Use when a single small service doesn't deserve its own compose file.
168
+ Any docker-compose attribute is valid inside `service: {...}`.
169
+
170
+ ### Form 4 — multi-service map
171
+
172
+ ```yaml
173
+ apps:
174
+ backend:
175
+ vars: { DATABASE_URL: ... } # shared by every service below
176
+ ports: [ "8000:8000" ] # default for primary; overridable below
177
+ services:
178
+ backend: # primary
179
+ image: ${IMAGE}
180
+ command: [ uvicorn, app.main:app ]
181
+ celery-worker:
182
+ image: ${IMAGE}
183
+ command: [ celery, -A, app.tasks, worker ]
184
+ depends_on: [ backend ]
185
+ vars: { CELERY_LOG_LEVEL: info } # per-service override (merge)
186
+ ports: [ ] # opt out of inherited ports
187
+ celery-beat:
188
+ image: ${IMAGE}
189
+ command: [ celery, -A, app.tasks, beat ]
190
+ depends_on: [ backend ]
191
+ ports: [ ]
192
+ ```
193
+
194
+ Use for compound apps (web + workers + beat, api + sidecar). Each
195
+ service value can mix compose-syntax with cupli-only `vars` (merged
196
+ into app-level) and `ports` (replaces app-level when present).
197
+
198
+ ### Form 4 — list shorthand
199
+
200
+ ```yaml
201
+ apps:
202
+ fleet:
203
+ composes: [ ${ APP_PATH }/docker-compose.yml ] # services declared in the compose-fragment
204
+ services:
205
+ - api
206
+ - worker
207
+ - beat
208
+ ```
209
+
210
+ Equivalent to `services: {api: {}, worker: {}, beat: {}}`. Use when
211
+ each service in the compound app just inherits app-level `vars` /
212
+ `ports` with no per-service tweaks.
213
+
214
+ ---
215
+
216
+ ## Common edit patterns (prompts you'll see)
217
+
218
+ ### "Add a celery worker"
219
+
220
+ If the app uses Form 1/2/3 → migrate to Form 4 (`services:` map),
221
+ moving the existing service spec into the map under the primary name.
222
+ Add the worker as another entry with `command: [celery, ...]` and
223
+ `depends_on: [<primary>]`.
224
+
225
+ ### "Pin everything to branch X"
226
+
227
+ Add `branch: X` to every `apps.<name>` and `mounts.<name>` with `repo:`.
228
+ Then `cupli git checkout X` to switch existing working trees.
229
+
230
+ ### "Add a one-off migration step"
231
+
232
+ ```yaml
233
+ apps:
234
+ migrate:
235
+ repo: <same as backend>
236
+ path: ${BACKEND_APP_PATH} # share working tree with backend
237
+ bases: [ python_runtime, pg_client ]
238
+ deps:
239
+ postgres: [ default ]
240
+ mode: oneshot # exits when done; blocks dependents
241
+ composes: [ ${ APP_PATH }/docker-compose.yml ]
242
+ ```
243
+
244
+ Then add to dependents:
245
+
246
+ ```yaml
247
+ apps:
248
+ backend:
249
+ deps:
250
+ migrate: [ default ] # backend waits for migrate to finish
251
+ ```
252
+
253
+ ### "Share a SDK between two apps via mount"
254
+
255
+ ```yaml
256
+ mounts:
257
+ shared-sdk:
258
+ repo: git@github.com:.../shared-sdk.git
259
+ branch: main
260
+ hosted_in: [ shop-web, shop-api ]
261
+ exec_path: /opt/shared-sdk
262
+ mode: rw
263
+ ```
264
+
265
+ Toggle: `cupli mounts attach shared-sdk` / `cupli mounts detach shared-sdk`.
266
+
267
+ ### "Add a top-level command for linting"
268
+
269
+ ```yaml
270
+ commands:
271
+ lint:
272
+ container: backend # app name (NOT compose service name)
273
+ run: ruff check src tests
274
+ help: Lint the backend.
275
+ top_level: true # `cupli lint` works alongside `cupli sc lint`
276
+ ```
277
+
278
+ ---
279
+
280
+ ## How cupli evaluates a space file (so you understand what's possible)
281
+
282
+ 1. **Parse YAML** with ruamel for line/col tracking.
283
+ 2. **Validate** against `SpaceModel` (Pydantic). Failure → `E002`
284
+ with per-field `file:line:col`.
285
+ 3. **Resolve space scope** — `auto-vars` + `<NAME>_*_PATH` defaults +
286
+ `envs:` files + `vars:` (in order, later wins).
287
+ 4. **Resolve bases** in C3 linearisation order, each producing its own
288
+ resolved scope.
289
+ 5. **Resolve apps** — each app picks its base chain, merges scopes,
290
+ substitutes `${VAR}` everywhere.
291
+ 6. **Resolve mounts** — analogously.
292
+ 7. **Render overrides** into `.locals/<space>/state/`.
293
+ 8. **`docker compose` invocation** with `COMPOSE_FILE` chaining:
294
+ `pre → bases.composes → apps.composes → inline → post`.
295
+
296
+ ---
297
+
298
+ ## Tools you have available
299
+
300
+ Run these to verify your edits:
301
+
302
+ ```bash
303
+ cupli space doctor # validate the whole space (path checks etc.)
304
+ cupli config # print merged docker-compose config
305
+ cupli graph # tree of bases/apps/mounts/commands
306
+ cupli env # full resolved env-file
307
+ cupli env -c <app> # an app's resolved scope
308
+ cupli --strict-vars <cmd> # promote ${UNKNOWN} warnings to errors
309
+ cupli -V # version (script-friendly)
310
+ ```
311
+
312
+ When something fails, the error has a numbered code:
313
+
314
+ ```
315
+ E002 Validation failed
316
+ /path/to/space.cupli.yaml: 1 error(s)
317
+ • apps.api.mode: Input should be 'up', 'oneshot' or 'disabled' (...:6:5)
318
+ hint: Fix the fields listed above (each entry includes file:line:col when known).
319
+ ```
320
+
321
+ `cupli explain <code>` for the full reference.
322
+
323
+ ---
324
+
325
+ ## Error codes you'll meet most
326
+
327
+ | Code | Cause | Fix |
328
+ |--------|----------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
329
+ | `E001` | Space file not found. | Check path. |
330
+ | `E002` | Validation. | Read per-field messages — they include line numbers. |
331
+ | `E014` | `${VAR}` cycle. | Break the cycle. |
332
+ | `E015` | User var shadows reserved name. | Rename the var or pass `--allow-shadow`. |
333
+ | `E016` | Unknown `${VAR}` in strict mode. | Define the var or remove the reference. |
334
+ | `E017` | `git clone` failed. | Check repo URL, branch, SSH access. |
335
+ | `E020` | Unknown name. | Check spelling against `cupli --list` or `cupli graph`. |
336
+ | `E029` | Space file already exists. | `cupli init --force` to overwrite. |
337
+ | `E030` | Per-component env-var name collision. | Rename one of the components (`shop-api` and `shop_api` both → `SHOP_API_APP_PATH`). |
338
+ | `E031` | Planned service not declared anywhere. | Either add it to a compose-fragment listed under `composes:` (and run `cupli space sync` if the repo isn't cloned yet), or supply at least one inline compose field (e.g. `image:`) under `service:` / `services.<name>`. |
339
+
340
+ ---
341
+
342
+ ## Idioms
343
+
344
+ * `${REGISTRY}/<app>:<tag>` — image name templates.
345
+ * `${APPS_PATH}/<name>` or `${<NAME>_APP_PATH}` — refer to a sibling app's path.
346
+ * `${USER_ID:-1000}` — fall-back for envs that might not be set.
347
+ * `depends_on: [<service-name>]` — plain compose syntax inside an
348
+ inline service spec. NOT to be confused with cupli's
349
+ `apps.<name>.deps:` which is at the app level (across apps).
350
+
351
+ ---
352
+
353
+ ## Things to remember if you're tempted to "fix" something
354
+
355
+ * **`override.*.yml` files in `.locals/` are generated.** Don't edit
356
+ them. Edit `space.cupli.yaml` and re-run cupli.
357
+ * **A pinned `branch:` does NOT auto-switch the working tree.** Cupli
358
+ reports drift; you opt in to switch via `cupli git checkout`.
359
+ * **`vars:` at app-level land in `environment:` of every service the
360
+ app drives**, plus the env-file. Don't repeat them in compose-syntax
361
+ `environment:` — you'll create duplicates (harmless but noisy).
362
+ * **`ports: []` is explicit "no ports"**, not "default ports". It opts
363
+ out of inherited app-level `ports`.
364
+ * **`service: dict` and `services: map` are mutually exclusive.** Pick
365
+ one. If you migrate from `service: dict` to `services:`, move the
366
+ whole compose spec under the new entry whose key = the desired
367
+ compose service name.
368
+ * **`cupli up <name>` accepts both app names and individual compose-service
369
+ names from compound apps.** Targeting `cupli up celery-worker` starts
370
+ only that one service (without firing up `celery-beat` and `backend`).
371
+ * **`cupli up --mode <m>` filters cross-app deps.** Without it, deps
372
+ inclusion follows the universe (all active apps). With a mode, only
373
+ deps whose mode-list contains `<m>` are walked.
374
+
375
+ ---
376
+
377
+ ## Where to look for more
378
+
379
+ * `space.cupli.yaml` at repo root — full reference with comments on
380
+ every key.
381
+ * `examples/{minimal,celery,multi-repo-shop,full-reference}/` — worked
382
+ examples by complexity.
383
+ * `README.md` / `README.ru.md` — user-facing docs.
384
+ * `cupli --list` — every CLI command grouped by area.
385
+ * `cupli explain <code>` — error code lookup.
cupli-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 extralait-web
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cupli-0.1.1/Makefile ADDED
@@ -0,0 +1,113 @@
1
+ # Makefile, adopted from pydantic Makefile (https://github.com/pydantic/pydantic/blob/main/Makefile).
2
+
3
+ .DEFAULT_GOAL := all
4
+ sources = src/cupli tests
5
+ NUM_THREADS?=1
6
+
7
+ .PHONY: .uv ## Check that uv is installed
8
+ .uv:
9
+ @uv -V || echo 'Please install uv: https://docs.astral.sh/uv/getting-started/installation/'
10
+
11
+ .PHONY: .pre-commit ## Check that pre-commit is installed
12
+ .pre-commit: .uv
13
+ @uv run pre-commit -V || uv pip install pre-commit
14
+
15
+ .PHONY: install ## Install the package, dependencies, and pre-commit for local development
16
+ install: .uv
17
+ uv sync --frozen --all-groups --all-packages --all-extras
18
+ uv pip install pre-commit
19
+ uv run pre-commit install --install-hooks
20
+
21
+ .PHONY: upgrade-lock ## Upgrade lock from scratch, updating all dependencies
22
+ upgrade-lock: .uv
23
+ uv lock --upgrade
24
+
25
+ .PHONY: format ## Auto-format python source files
26
+ format: .uv
27
+ uv run ruff check --fix $(sources)
28
+ uv run ruff format $(sources)
29
+
30
+ .PHONY: lint ## Lint source files
31
+ lint: .uv
32
+ uv run ruff check $(sources)
33
+ uv run ruff format --check $(sources)
34
+
35
+ .PHONY: codespell ## Use Codespell to do spellchecking
36
+ codespell: .pre-commit
37
+ uv run pre-commit run codespell --all-files
38
+
39
+ .PHONY: typecheck ## Perform type-checking
40
+ typecheck: .pre-commit
41
+ uv run pre-commit run typecheck --all-files
42
+
43
+ .PHONY: schema ## Regenerate space.schema.json from the Pydantic models
44
+ schema: .uv
45
+ uv run python scripts/generate_schema.py
46
+
47
+ .PHONY: examples-validate ## Validate every space.cupli.yaml under docs/examples/
48
+ examples-validate: .uv
49
+ @for ex in docs/examples/*/space.cupli.yaml; do \
50
+ echo "validating $$ex"; \
51
+ uv run python -m cupli -f $$ex graph >/dev/null || exit 1; \
52
+ done
53
+ @echo "all examples valid."
54
+
55
+ .PHONY: test ## Run all tests
56
+ test: .uv
57
+ uv run coverage run -m pytest --durations=10
58
+
59
+ .PHONY: test-verbose ## Run all tests, more verbose
60
+ test-verbose: .uv
61
+ uv run coverage run -m pytest --durations=10 -vvvrP
62
+
63
+ .PHONY: smoke ## Run docker-marked integration tests (skipped without a docker daemon)
64
+ smoke: .uv
65
+ uv run pytest -o 'addopts=' -m docker --durations=10 -rs
66
+
67
+ .PHONY: testcov ## Run tests and generate a coverage report
68
+ testcov: test
69
+ @echo "building coverage html"
70
+ @uv run coverage html
71
+ @echo "building coverage lcov"
72
+ @uv run coverage lcov
73
+
74
+ .PHONY: testcov-verbose ## Run tests and generate a coverage report, more verbose
75
+ testcov-verbose: test-verbose
76
+ @echo "building coverage html"
77
+ @uv run coverage html
78
+ @echo "building coverage lcov"
79
+ @uv run coverage lcov
80
+
81
+ .PHONY: shell ## Run IPython
82
+ shell: .uv
83
+ uv run ipython
84
+
85
+ .PHONY: all ## Run the standard set of checks performed in CI
86
+ all: schema lint typecheck codespell testcov examples-validate
87
+
88
+ .PHONY: clean ## Clear local caches and build artifacts
89
+ clean:
90
+ rm -rf `find . -name __pycache__`
91
+ rm -f `find . -type f -name '*.py[co]'`
92
+ rm -f `find . -type f -name '*~'`
93
+ rm -f `find . -type f -name '.*~'`
94
+ rm -rf .cache
95
+ rm -rf .pytest_cache
96
+ rm -rf .ruff_cache
97
+ rm -rf htmlcov
98
+ rm -rf *.egg-info
99
+ rm -f .coverage
100
+ rm -f .coverage.*
101
+ rm -rf build
102
+ rm -rf dist
103
+ rm -rf site
104
+ rm -rf docs/_build
105
+ rm -rf docs/.changelog.md docs/.version.md docs/.tmp_schema_mappings.html
106
+ rm -rf coverage.xml
107
+
108
+ .PHONY: help ## Display this message
109
+ help:
110
+ @grep -E \
111
+ '^.PHONY: .*?## .*$$' $(MAKEFILE_LIST) | \
112
+ sort | \
113
+ awk 'BEGIN {FS = ".PHONY: |## "}; {printf "\033[36m%-19s\033[0m %s\n", $$2, $$3}'