Skip to content

Commit e0222d0

Browse files
committed
feat: log convert metrics to benchmark local runs
1 parent b59afed commit e0222d0

12 files changed

+3946
-1380
lines changed

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,9 @@ tests-output/
211211
# uv
212212
uv.lock
213213
.vscode/settings.json
214+
215+
*.zarr
216+
.DS_§tore
217+
out
218+
runs
219+
*.gz

Dockerfile

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# ========= stage: builder =========
2+
FROM python:3.11-slim AS builder
3+
4+
ENV PYTHONUNBUFFERED="1" \
5+
PIP_NO_CACHE_DIR="1"
6+
7+
# Build arg: 0=wheel-only (small; build as linux/amd64), 1=portable (adds GDAL/PROJ & toolchain)
8+
ARG PORTABLE_BUILD=0
9+
10+
# Base OS deps
11+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
12+
ca-certificates curl git \
13+
&& rm -rf /var/lib/apt/lists/*
14+
15+
WORKDIR /app
16+
17+
# uv + modern pip
18+
COPY pyproject.toml uv.lock ./
19+
RUN pip install --no-cache-dir -U pip \
20+
&& pip install --no-cache-dir uv \
21+
&& uv --version
22+
23+
# ---- Export ONLY third-party runtime deps (no hashes). We filter out any editable/self lines.
24+
# NOTE: We export BEFORE copying src/ to avoid uv deciding this is a local edit; still filter to be safe.
25+
RUN set -euo pipefail; \
26+
uv export --no-group dev --no-group test --format=requirements-txt --no-hashes -o /tmp/req.raw.txt; \
27+
awk ' \
28+
BEGIN{IGNORECASE=1} \
29+
# drop editable flags
30+
/^-e[[:space:]]/ || /^--editable[[:space:]]/ {next} \
31+
# drop local/self refs
32+
/@ file:/ || /file:\/\// {next} \
33+
# drop our own package if present
34+
/^eopf-geozarr([[:space:]]|==|$)/ {next} \
35+
# drop comments/blank lines
36+
/^[[:space:]]*#/ || /^[[:space:]]*$/ {next} \
37+
{print} \
38+
' /tmp/req.raw.txt > /tmp/requirements.txt; \
39+
echo "----- filtered requirements (head) -----"; \
40+
sed -n '1,80p' /tmp/requirements.txt
41+
42+
# ---- If PORTABLE, add toolchain + GDAL/PROJ for sdist builds (arm64 etc.)
43+
RUN if [ "$PORTABLE_BUILD" = "1" ]; then \
44+
apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
45+
build-essential gdal-bin libgdal-dev proj-bin libproj-dev \
46+
&& rm -rf /var/lib/apt/lists/* ; \
47+
fi
48+
49+
# ---- Install third-party deps, then your package (no re-resolve of deps)
50+
RUN if [ "$PORTABLE_BUILD" = "1" ]; then \
51+
pip install --no-cache-dir -r /tmp/requirements.txt ; \
52+
else \
53+
PIP_ONLY_BINARY=":all:" pip install --no-cache-dir --prefer-binary -r /tmp/requirements.txt ; \
54+
fi
55+
56+
# Now copy source and install the project itself without deps (pure python install)
57+
COPY src ./src
58+
RUN pip install --no-cache-dir --no-deps .
59+
60+
# Optional: byte-compile
61+
RUN python -m compileall -q /usr/local/lib/python3.11/site-packages || true
62+
63+
# ========= stage: runtime =========
64+
FROM python:3.11-slim AS runtime
65+
ENV PYTHONUNBUFFERED="1"
66+
67+
# Tiny libs manylinux wheels often need
68+
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
69+
libstdc++6 libgomp1 \
70+
&& rm -rf /var/lib/apt/lists/*
71+
72+
WORKDIR /app
73+
COPY --from=builder /usr/local /usr/local
74+
75+
# Argo Script template supplies the command
76+
CMD ["python", "-V"]

Makefile

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# ===== Config =====
2+
IMAGE ?= eopf-geozarr:dev
3+
NAMESPACE ?= argo # Kubernetes namespace where Argo runs
4+
TPL ?= geozarr-convert-template.yaml
5+
PARAMS ?= params.json
6+
CLUSTER ?= k3s-default
7+
8+
# Runtime param overrides (env > PARAMS file)
9+
STAC_URL ?=
10+
OUTPUT_ZARR ?=
11+
GROUPS ?=
12+
13+
# Abbrev: WF = Workflow name; PVC = PersistentVolumeClaim (<WF>-outpvc)
14+
15+
.PHONY: build load-k3d load-minikube argo-install template apply \
16+
submit submit-cli submit-api status latest logs-save clean \
17+
_ensure-dirs fetch-tar run clean-pvc
18+
19+
# Build the image locally
20+
# make build -> WHEEL mode (small), builds linux/amd64
21+
# make build PORTABLE=1 -> PORTABLE mode (bigger), builds for native arch
22+
build:
23+
@if [ "$(PORTABLE)" = "1" ]; then \
24+
echo "==> Building PORTABLE image for native platform (allows source builds)"; \
25+
docker build \
26+
--build-arg PORTABLE_BUILD=1 \
27+
-t $(IMAGE) . ; \
28+
else \
29+
echo "==> Building WHEEL image for linux/amd64 (prebuilt wheels)"; \
30+
docker buildx build --platform=linux/amd64 \
31+
--build-arg PORTABLE_BUILD=0 \
32+
-t $(IMAGE) --load . ; \
33+
fi
34+
35+
# Load image into k3d’s containerd (dev clusters)
36+
load-k3d:
37+
k3d image import $(IMAGE) --cluster $(CLUSTER) || \
38+
(docker save $(IMAGE) | docker exec -i $$(docker ps --format '{{.Names}}' | grep $(CLUSTER)-server-0) ctr -n k8s.io images import -)
39+
40+
# Build the image inside minikube’s Docker
41+
load-minikube:
42+
eval "$$(minikube docker-env)"; docker build -t $(IMAGE) .
43+
44+
# Install Argo Workflows (v3.7.1) into $(NAMESPACE)
45+
argo-install:
46+
kubectl create ns $(NAMESPACE) 2>/dev/null || true
47+
kubectl apply -n $(NAMESPACE) -f https://github.com/argoproj/argo-workflows/releases/download/v3.7.1/install.yaml
48+
kubectl -n $(NAMESPACE) rollout status deploy/workflow-controller
49+
kubectl -n $(NAMESPACE) rollout status deploy/argo-server
50+
51+
# Apply (or update) the WorkflowTemplate
52+
template:
53+
kubectl -n $(NAMESPACE) apply -f $(TPL)
54+
kubectl -n $(NAMESPACE) get workflowtemplate geozarr-convert
55+
56+
# Build + load + install + template (one shot)
57+
apply: build load-k3d argo-install template
58+
59+
# Submit via CLI (uses env overrides, else PARAMS file)
60+
submit: _ensure-dirs
61+
@STAC="$${STAC_URL:-$$(jq -r '.arguments.parameters[] | select(.name=="stac_url").value' $(PARAMS))}"; \
62+
OUT="$${OUTPUT_ZARR:-$$(jq -r '.arguments.parameters[] | select(.name=="output_zarr").value' $(PARAMS))}"; \
63+
GRP="$${GROUPS:-$$(jq -r '.arguments.parameters[] | select(.name=="groups").value' $(PARAMS))}"; \
64+
echo "Submitting:"; echo " stac_url=$$STAC"; echo " output_zarr=$$OUT"; echo " groups=$$GRP"; \
65+
WF=$$(argo submit -n $(NAMESPACE) --from workflowtemplate/geozarr-convert \
66+
-p stac_url="$$STAC" -p output_zarr="$$OUT" -p groups="$$GRP" -o name); \
67+
TSTAMP=$$(date +%Y%m%d-%H%M%S); \
68+
argo get -n $(NAMESPACE) $$WF -o json > runs/$${TSTAMP}-$${WF##*/}.json; \
69+
argo get -n $(NAMESPACE) $$WF --output wide | tee runs/$${TSTAMP}-$${WF##*/}.summary.txt; \
70+
echo "Workflow: $$WF"
71+
72+
# Submit via CLI (PARAMS file only, no env overrides)
73+
submit-cli: _ensure-dirs
74+
@WF=$$(argo submit -n $(NAMESPACE) --from workflowtemplate/geozarr-convert \
75+
-p stac_url="$$(jq -r '.arguments.parameters[] | select(.name=="stac_url").value' $(PARAMS))" \
76+
-p output_zarr="$$(jq -r '.arguments.parameters[] | select(.name=="output_zarr").value' $(PARAMS))" \
77+
-p groups="$$(jq -r '.arguments.parameters[] | select(.name=="groups").value' $(PARAMS))" \
78+
-o name); \
79+
TSTAMP=$$(date +%Y%m%d-%H%M%S); \
80+
argo get -n $(NAMESPACE) $$WF -o json > runs/$${TSTAMP}-$${WF##*/}.json; \
81+
argo get -n $(NAMESPACE) $$WF --output wide | tee runs/$${TSTAMP}-$${WF##*/}.summary.txt; \
82+
echo "Workflow: $$WF"
83+
84+
# Submit via Argo Server HTTP (dev port-forward, no token)
85+
submit-api: _ensure-dirs
86+
kubectl -n $(NAMESPACE) port-forward svc/argo-server 2746:2746 >/dev/null 2>&1 & echo $$! > .pf.pid
87+
sleep 1
88+
curl -s -H 'Content-Type: application/json' \
89+
--data-binary @$(PARAMS) \
90+
http://localhost:2746/api/v1/workflows/$(NAMESPACE)/submit \
91+
| tee runs/submit-response.json | jq . >/dev/null || \
92+
(echo "Non-JSON response (see runs/submit-response.json)"; exit 1)
93+
-@[ -f .pf.pid ] && kill $$(cat .pf.pid) 2>/dev/null || true
94+
-@rm -f .pf.pid
95+
96+
# Inspect
97+
status:
98+
argo list -n $(NAMESPACE); echo; kubectl -n $(NAMESPACE) get wf
99+
100+
latest:
101+
argo get -n $(NAMESPACE) @latest --output wide
102+
103+
logs-save: _ensure-dirs
104+
@WF=$$(argo list -n $(NAMESPACE) --output name | tail -1); \
105+
TSTAMP=$$(date +%Y%m%d-%H%M%S); \
106+
argo logs -n $(NAMESPACE) $$WF -c main > logs/$${TSTAMP}-$${WF##*/}.log; \
107+
echo "Wrote logs/$${TSTAMP}-$${WF##*/}.log"
108+
109+
# Delete all workflows + completed pods
110+
clean:
111+
argo delete -n $(NAMESPACE) --all || true
112+
kubectl -n $(NAMESPACE) delete pod -l workflows.argoproj.io/completed=true --force --grace-period=0 || true
113+
114+
_ensure-dirs:
115+
@mkdir -p runs logs
116+
117+
# Fetch from PVC: copy tarball, unpack into runs/<WF>/, pull any extra files on /outputs
118+
fetch-tar: _ensure-dirs
119+
@WF=$$(argo list -n $(NAMESPACE) --output name | tail -1 | sed 's#.*/##'); \
120+
PVC="$$WF-outpvc"; OUTDIR="runs/$$WF"; \
121+
echo "Workflow: $$WF"; echo "PVC: $$PVC"; mkdir -p $$OUTDIR; \
122+
kubectl -n $(NAMESPACE) delete pod fetch-$$WF --ignore-not-found >/dev/null 2>&1 || true; \
123+
cat <<'YAML' | sed "s/{{WF}}/$$WF/g" | sed "s/{{PVC}}/$$PVC/g" | kubectl -n $(NAMESPACE) apply -f -
124+
apiVersion: v1
125+
kind: Pod
126+
metadata:
127+
name: fetch-{{WF}}
128+
spec:
129+
restartPolicy: Never
130+
containers:
131+
- name: fetch
132+
image: busybox:1.36
133+
command: ["sh","-lc","sleep 600"]
134+
volumeMounts:
135+
- name: out
136+
mountPath: /mnt/out
137+
volumes:
138+
- name: out
139+
persistentVolumeClaim:
140+
claimName: {{PVC}}
141+
YAML
142+
kubectl -n $(NAMESPACE) wait --for=condition=Ready pod/fetch-$$WF --timeout=60s
143+
# Main artifact
144+
kubectl -n $(NAMESPACE) cp fetch-$$WF:/mnt/out/geozarr.tar.gz $$OUTDIR/geozarr.tar.gz
145+
tar -xzf $$OUTDIR/geozarr.tar.gz -C $$OUTDIR
146+
# Copy any other files (e.g., dask-report.html)
147+
kubectl -n $(NAMESPACE) cp fetch-$$WF:/mnt/out/. $$OUTDIR/ || true
148+
kubectl -n $(NAMESPACE) delete pod fetch-$$WF --wait=false
149+
@echo "Unpacked into $$OUTDIR/"
150+
151+
# Convenience: build + load + template + submit + fetch
152+
run: apply submit fetch-tar
153+
154+
# Cleanup stray per-run PVCs (removes stored artifacts)
155+
clean-pvc:
156+
kubectl -n $(NAMESPACE) delete pvc -l workflows.argoproj.io/workflow 2>/dev/null || true
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Pipeline Integration Recommendations
2+
3+
This document lists enhancements observed while integrating `eopf-geozarr` into an Argo-based batch conversion pipeline (data-model-pipeline). They are candidates for upstream inclusion or API refinement.
4+
5+
## 1. Output Prefix Expansion
6+
**Current (pipeline)**: Wrapper detects `output_zarr` ending with `/` and appends `<item_id>_geozarr.zarr` derived from the input STAC/Zarr URL.
7+
**Recommendation**: Support `--output-prefix` OR accept a trailing slash on positional `output_path` and perform the expansion internally (emitting the resolved final path). Add a log line: `Resolved output store: s3://.../S2A_..._geozarr.zarr`.
8+
9+
## 2. Group Existence Validation (Pre-flight)
10+
**Current (pipeline)**: `validate_params_groups.py` inspects filesystem structure (presence of `.zarray` / `.zgroup`).
11+
**Recommendation**: Native CLI flag `--validate-groups` to prune or fail fast when groups don’t exist. Modes:
12+
- `--validate-groups=warn` (default): drop missing, report.
13+
- `--validate-groups=error`: abort if any missing.
14+
Emit JSON or structured summary when `--verbose`.
15+
16+
## 3. Profiles (WOZ Profiles)
17+
**Current (pipeline)**: External JSON profile expansion before calling CLI.
18+
**Recommendation**: Provide `--profile <name>` in CLI mapping to preset groups + chunk params. Add `eopf-geozarr profile list` / `profile show <name>` subcommands. Keep external mechanism as fallback.
19+
20+
## 4. Compressor Handling
21+
**Current**: Template attempted to pass `--compressor`; CLI does not expose codec choice.
22+
**Recommendation**: If codec selection is desired, add `--compressor <name>` now (zstd, lz4, blosc) with validation; else document fixed default explicitly to avoid confusion.
23+
24+
## 5. CRS Groups Convenience
25+
**Current**: `--crs-groups` optional list.
26+
**Recommendation**: Discover candidate groups automatically (search for geometry-like datasets) unless `--crs-groups` provided (override). Provide `eopf-geozarr info --crs-scan` to preview.
27+
28+
## 6. Dask Cluster Ergonomics
29+
**Current**: `--dask-cluster` toggles local cluster with no feedback.
30+
**Recommendation**: Print cluster dashboard URL (if available) and add `--dask-workers N` for quick scaling.
31+
32+
## 7. Structured Logging / Run Metadata
33+
**Current**: Plain prints; pipeline scrapes logs.
34+
**Recommendation**: Optional `--run-metadata <path.json>` to write machine-readable summary: inputs, resolved groups, timings, warnings. Eases automation and reproducibility.
35+
36+
## 8. Validation Command Enhancements
37+
**Current**: `validate` skeleton present but incomplete.
38+
**Recommendation**:
39+
- Implement spec checks (multiscales, attributes, chunk shape policy).
40+
- Exit code non-zero on *hard* failures, zero with warnings for soft issues.
41+
- `--format json` for programmatic consumption.
42+
43+
## 9. HTML Tree Generation
44+
**Current**: `_generate_html_output` scaffold incomplete.
45+
**Recommendation**: Finish implementation; integrate with `info --html-output`. Provide minimal inline CSS (already drafted) and optional `--open-browser` flag.
46+
47+
## 10. Progress Reporting
48+
**Recommendation**: Emit periodic progress per group: `group=/measurements/r10m scale=2 written=...MB elapsed=...s` to assist monitoring in batch workflows.
49+
50+
## 11. Retry / Resumability
51+
**Recommendation**: Add `--resume` to skip already existing multiscale levels if output store partially present.
52+
53+
## 12. Exit Codes (Contract)
54+
Document exit code meanings:
55+
- 0 success
56+
- 2 validation (input) error
57+
- 3 group resolution failure
58+
- 4 conversion runtime error
59+
60+
## 13. Environment Variable Overrides
61+
Allow `EOZ_DEFAULT_PROFILE`, `EOZ_OUTPUT_PREFIX` env vars as implicit defaults (still overridden by flags).
62+
63+
## 14. Example Invocation Block in README
64+
Provide ready-to-copy examples for Sentinel-2 & Sentinel-1 including polarization groups when Phase 1 logic is public.
65+
66+
---
67+
**Next Steps (Suggested Order)**
68+
1. Implement output prefix expansion (low risk, high UX win)
69+
2. Group validation flag (prevents silent empty writes)
70+
3. Finish validate + info HTML features
71+
4. Add structured run metadata output
72+
5. Introduce profiles subcommands then deprecate external expansion path over time
73+
74+
Feedback welcome—pipeline experience will continue surfacing actionable deltas.

0 commit comments

Comments
 (0)