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.
- cupli-0.1.1/.gitignore +48 -0
- cupli-0.1.1/AGENTS.md +385 -0
- cupli-0.1.1/LICENSE +21 -0
- cupli-0.1.1/Makefile +113 -0
- cupli-0.1.1/PKG-INFO +906 -0
- cupli-0.1.1/README.md +747 -0
- cupli-0.1.1/pyproject.toml +252 -0
- cupli-0.1.1/src/cupli/__init__.py +1 -0
- cupli-0.1.1/src/cupli/__main__.py +6 -0
- cupli-0.1.1/src/cupli/cli/__init__.py +7 -0
- cupli-0.1.1/src/cupli/cli/_completion.py +157 -0
- cupli-0.1.1/src/cupli/cli/container.py +28 -0
- cupli-0.1.1/src/cupli/cli/dashboard.py +106 -0
- cupli-0.1.1/src/cupli/cli/diagnostics.py +74 -0
- cupli-0.1.1/src/cupli/cli/exec.py +364 -0
- cupli-0.1.1/src/cupli/cli/git.py +189 -0
- cupli-0.1.1/src/cupli/cli/hooks.py +133 -0
- cupli-0.1.1/src/cupli/cli/lifecycle.py +232 -0
- cupli-0.1.1/src/cupli/cli/mounts.py +114 -0
- cupli-0.1.1/src/cupli/cli/root.py +395 -0
- cupli-0.1.1/src/cupli/cli/workspace.py +459 -0
- cupli-0.1.1/src/cupli/core/__init__.py +1 -0
- cupli-0.1.1/src/cupli/core/c3.py +116 -0
- cupli-0.1.1/src/cupli/core/cache.py +149 -0
- cupli-0.1.1/src/cupli/core/env_resolver.py +189 -0
- cupli-0.1.1/src/cupli/core/loader.py +461 -0
- cupli-0.1.1/src/cupli/core/parser.py +159 -0
- cupli-0.1.1/src/cupli/core/registry.py +277 -0
- cupli-0.1.1/src/cupli/domain/__init__.py +1 -0
- cupli-0.1.1/src/cupli/domain/consts.py +137 -0
- cupli-0.1.1/src/cupli/domain/enums.py +58 -0
- cupli-0.1.1/src/cupli/domain/errors.py +285 -0
- cupli-0.1.1/src/cupli/domain/models.py +477 -0
- cupli-0.1.1/src/cupli/domain/plan.py +92 -0
- cupli-0.1.1/src/cupli/domain/runtime.py +42 -0
- cupli-0.1.1/src/cupli/manage.py +10 -0
- cupli-0.1.1/src/cupli/services/__init__.py +1 -0
- cupli-0.1.1/src/cupli/services/compose_service.py +745 -0
- cupli-0.1.1/src/cupli/services/filter_service.py +140 -0
- cupli-0.1.1/src/cupli/services/git_service.py +366 -0
- cupli-0.1.1/src/cupli/services/hooks_service.py +472 -0
- cupli-0.1.1/src/cupli/services/ide_setup_service.py +233 -0
- cupli-0.1.1/src/cupli/services/mounts_service.py +122 -0
- cupli-0.1.1/src/cupli/services/workspace_service.py +329 -0
- cupli-0.1.1/src/cupli/utils/__init__.py +1 -0
- cupli-0.1.1/src/cupli/utils/console.py +161 -0
- cupli-0.1.1/src/cupli/utils/exceptions.py +90 -0
- cupli-0.1.1/src/cupli/utils/fuzzy.py +39 -0
- cupli-0.1.1/src/cupli/utils/git.py +141 -0
- cupli-0.1.1/src/cupli/utils/json.py +14 -0
- cupli-0.1.1/src/cupli/utils/lock.py +101 -0
- cupli-0.1.1/src/cupli/utils/path.py +85 -0
- cupli-0.1.1/src/cupli/utils/subprocess.py +74 -0
- cupli-0.1.1/src/cupli/version.py +42 -0
- cupli-0.1.1/tests/__init__.py +0 -0
- cupli-0.1.1/tests/cli/__init__.py +0 -0
- cupli-0.1.1/tests/cli/test_exec.py +233 -0
- cupli-0.1.1/tests/cli/test_hooks.py +73 -0
- cupli-0.1.1/tests/cli/test_mounts.py +80 -0
- cupli-0.1.1/tests/cli/test_root.py +82 -0
- cupli-0.1.1/tests/cli/test_workspace.py +265 -0
- cupli-0.1.1/tests/conftest.py +37 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/bad_name.yaml +3 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/bad_version.yaml +4 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/bad_yaml_syntax.yaml +4 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/command_unknown_container.yaml +7 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/empty.yaml +1 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/missing_apps.yaml +3 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/mount_relative_exec_path.yaml +7 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/mount_unknown_host.yaml +7 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/unknown_base.yaml +4 -0
- cupli-0.1.1/tests/fixtures/spaces/invalid/unknown_dep.yaml +5 -0
- cupli-0.1.1/tests/fixtures/spaces/minimal/space.cupli.yaml +4 -0
- cupli-0.1.1/tests/fixtures/spaces/with_bases/space.cupli.yaml +22 -0
- cupli-0.1.1/tests/fixtures/spaces/with_mounts/space.cupli.yaml +22 -0
- cupli-0.1.1/tests/integration/__init__.py +0 -0
- cupli-0.1.1/tests/integration/test_lifecycle.py +123 -0
- cupli-0.1.1/tests/unit/__init__.py +0 -0
- cupli-0.1.1/tests/unit/test_auto_register.py +67 -0
- cupli-0.1.1/tests/unit/test_c3.py +50 -0
- cupli-0.1.1/tests/unit/test_compose_service.py +770 -0
- cupli-0.1.1/tests/unit/test_env_resolver.py +193 -0
- cupli-0.1.1/tests/unit/test_excepthook.py +62 -0
- cupli-0.1.1/tests/unit/test_exceptions.py +64 -0
- cupli-0.1.1/tests/unit/test_filter_service.py +77 -0
- cupli-0.1.1/tests/unit/test_fuzzy.py +27 -0
- cupli-0.1.1/tests/unit/test_git_service.py +131 -0
- cupli-0.1.1/tests/unit/test_hooks_service.py +141 -0
- cupli-0.1.1/tests/unit/test_ide_setup_service.py +123 -0
- cupli-0.1.1/tests/unit/test_loader.py +197 -0
- cupli-0.1.1/tests/unit/test_lock.py +47 -0
- cupli-0.1.1/tests/unit/test_models_coercion.py +67 -0
- cupli-0.1.1/tests/unit/test_mounts_service.py +75 -0
- cupli-0.1.1/tests/unit/test_parser.py +165 -0
- cupli-0.1.1/tests/unit/test_registry.py +263 -0
- 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}'
|