composable-data-stack 0.5.0__tar.gz → 0.5.2__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.
- {composable_data_stack-0.5.0/composable_data_stack.egg-info → composable_data_stack-0.5.2}/PKG-INFO +16 -12
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/README.md +15 -11
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/getter.py +173 -45
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/main.py +38 -8
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2/composable_data_stack.egg-info}/PKG-INFO +16 -12
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/pyproject.toml +1 -1
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_getter.py +285 -16
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_main.py +56 -2
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/LICENSE +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/__init__.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/constants.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/diagnostics.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/graph.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/image_updates.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/image_verification.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/loader.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/overlay.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/planner.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/preflight.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/renderer.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/resolver.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/resources/__init__.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/resources/contract.schema.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/resources/module.schema.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/resources/profile.schema.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/resources/rule-schema.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/resources/rule-set.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/secrets.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/security.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/security_common.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/state.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/up_runner.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/utils.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/cli/validator.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/composable_data_stack.egg-info/SOURCES.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/composable_data_stack.egg-info/dependency_links.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/composable_data_stack.egg-info/entry_points.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/composable_data_stack.egg-info/requires.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/composable_data_stack.egg-info/top_level.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/setup.cfg +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_cds_workflow.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_completion.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_compose_runtime_smoke.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_dagster_config_generation.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_dagster_definitions_uri.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_dagster_entrypoint_backend_guard.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_dagster_hardening.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_environment_classification.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_environment_cli.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_graph.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_image_security_scan_workflow.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_image_updates.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_image_verification.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_module_isolation.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_modules_no_committed_secrets.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_mvp_proof_workflow.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_overlay.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_planner.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_preflight.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_publish_images_workflow.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_python_version_alignment.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_release_version_check.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_render_example_profile.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_renderer.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_resolve_profile_path.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_resolver.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_secrets.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_security.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_smoke_example_profile.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_state.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_superset_hardening.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_trivyignore.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_up_runner.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_upstream_image_hardening.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.2}/tests/test_validator.py +0 -0
{composable_data_stack-0.5.0/composable_data_stack.egg-info → composable_data_stack-0.5.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: composable-data-stack
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: CLI for validating and composing composable data stack profiles
|
|
5
5
|
Author: Ronald Hensbergen
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -55,6 +55,10 @@ Composable Data Stack (CDS) is a framework for defining and assembling data plat
|
|
|
55
55
|
|
|
56
56
|
> **Note:** Development helper tools are located in the `tools/` directory (git-ignored). See `tools/pr-cli/README.md` for PR creation scripts.
|
|
57
57
|
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## ⚡ Why CDS
|
|
61
|
+
|
|
58
62
|
Instead of hardcoding integrations or relying on fragile pipelines, CDS introduces:
|
|
59
63
|
|
|
60
64
|
- 🔧 **Modules**: reusable components (Dagster, Postgres, Superset)
|
|
@@ -63,10 +67,6 @@ Instead of hardcoding integrations or relying on fragile pipelines, CDS introduc
|
|
|
63
67
|
|
|
64
68
|
Think of it as Infrastructure as Code, but for data platforms.
|
|
65
69
|
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## ⚡ Why CDS
|
|
69
|
-
|
|
70
70
|
Modern data platforms force a trade-off:
|
|
71
71
|
|
|
72
72
|
|Approach|Problem|
|
|
@@ -642,7 +642,7 @@ before and never look for an `environments/` directory.
|
|
|
642
642
|
|
|
643
643
|
|Command|Description|
|
|
644
644
|
|---|---|
|
|
645
|
-
|cds get \<profile\> [--remote \<repo\>] [--into \<dir\>]|Fetch a profile plus its dependent module/runtime assets into a local CDS layout|
|
|
645
|
+
|cds get \<profile\> [--remote \<owner/repo\>] [--ref \<ref\>] [--local \<dir\>] [--into \<dir\>]|Fetch a profile plus its dependent module/runtime assets from GitHub into a local CDS layout|
|
|
646
646
|
|cds init [profile]|Generate a project `.env` template from profile secret definitions|
|
|
647
647
|
|cds validate [profile]|Validate modules and contracts|
|
|
648
648
|
|cds preflight [profile]|Check runtime tools, required environment values, and host ports without starting services|
|
|
@@ -663,12 +663,16 @@ all accept `--environment <name>` (or `-e <name>`) to merge
|
|
|
663
663
|
|
|
664
664
|
`cds get` copies the selected `profiles/<name>/` tree, every referenced module
|
|
665
665
|
directory, and any local build-context assets referenced by those modules'
|
|
666
|
-
Dockerfiles. By
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
666
|
+
Dockerfiles. By design it downloads from GitHub rather than a local checkout:
|
|
667
|
+
by default it fetches this project's upstream repository at the `main` branch,
|
|
668
|
+
downloading a tarball via the GitHub API (no `git` binary required). Use
|
|
669
|
+
`--remote <owner/repo>` (or a `github.com/...` URL) to fetch a fork, and
|
|
670
|
+
`--ref <branch|tag|sha>` to select a specific revision. Pass `--local <dir>`
|
|
671
|
+
to use an existing local directory instead of downloading (mutually exclusive
|
|
672
|
+
with `--remote`/`--ref`) for offline/dev workflows. Use `--into` to choose a
|
|
673
|
+
destination root, `--dry-run` to inspect the copy plan first, and `--force` to
|
|
674
|
+
replace conflicting local files. Successful fetches record tracking metadata
|
|
675
|
+
in `.cds/get-manifest.json` for future update workflows.
|
|
672
676
|
|
|
673
677
|
`[profile]` accepts:
|
|
674
678
|
|
|
@@ -19,6 +19,10 @@ Composable Data Stack (CDS) is a framework for defining and assembling data plat
|
|
|
19
19
|
|
|
20
20
|
> **Note:** Development helper tools are located in the `tools/` directory (git-ignored). See `tools/pr-cli/README.md` for PR creation scripts.
|
|
21
21
|
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## ⚡ Why CDS
|
|
25
|
+
|
|
22
26
|
Instead of hardcoding integrations or relying on fragile pipelines, CDS introduces:
|
|
23
27
|
|
|
24
28
|
- 🔧 **Modules**: reusable components (Dagster, Postgres, Superset)
|
|
@@ -27,10 +31,6 @@ Instead of hardcoding integrations or relying on fragile pipelines, CDS introduc
|
|
|
27
31
|
|
|
28
32
|
Think of it as Infrastructure as Code, but for data platforms.
|
|
29
33
|
|
|
30
|
-
---
|
|
31
|
-
|
|
32
|
-
## ⚡ Why CDS
|
|
33
|
-
|
|
34
34
|
Modern data platforms force a trade-off:
|
|
35
35
|
|
|
36
36
|
|Approach|Problem|
|
|
@@ -606,7 +606,7 @@ before and never look for an `environments/` directory.
|
|
|
606
606
|
|
|
607
607
|
|Command|Description|
|
|
608
608
|
|---|---|
|
|
609
|
-
|cds get \<profile\> [--remote \<repo\>] [--into \<dir\>]|Fetch a profile plus its dependent module/runtime assets into a local CDS layout|
|
|
609
|
+
|cds get \<profile\> [--remote \<owner/repo\>] [--ref \<ref\>] [--local \<dir\>] [--into \<dir\>]|Fetch a profile plus its dependent module/runtime assets from GitHub into a local CDS layout|
|
|
610
610
|
|cds init [profile]|Generate a project `.env` template from profile secret definitions|
|
|
611
611
|
|cds validate [profile]|Validate modules and contracts|
|
|
612
612
|
|cds preflight [profile]|Check runtime tools, required environment values, and host ports without starting services|
|
|
@@ -627,12 +627,16 @@ all accept `--environment <name>` (or `-e <name>`) to merge
|
|
|
627
627
|
|
|
628
628
|
`cds get` copies the selected `profiles/<name>/` tree, every referenced module
|
|
629
629
|
directory, and any local build-context assets referenced by those modules'
|
|
630
|
-
Dockerfiles. By
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
630
|
+
Dockerfiles. By design it downloads from GitHub rather than a local checkout:
|
|
631
|
+
by default it fetches this project's upstream repository at the `main` branch,
|
|
632
|
+
downloading a tarball via the GitHub API (no `git` binary required). Use
|
|
633
|
+
`--remote <owner/repo>` (or a `github.com/...` URL) to fetch a fork, and
|
|
634
|
+
`--ref <branch|tag|sha>` to select a specific revision. Pass `--local <dir>`
|
|
635
|
+
to use an existing local directory instead of downloading (mutually exclusive
|
|
636
|
+
with `--remote`/`--ref`) for offline/dev workflows. Use `--into` to choose a
|
|
637
|
+
destination root, `--dry-run` to inspect the copy plan first, and `--force` to
|
|
638
|
+
replace conflicting local files. Successful fetches record tracking metadata
|
|
639
|
+
in `.cds/get-manifest.json` for future update workflows.
|
|
636
640
|
|
|
637
641
|
`[profile]` accepts:
|
|
638
642
|
|
|
@@ -2,17 +2,35 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
import shlex
|
|
6
7
|
import shutil
|
|
8
|
+
import sys
|
|
9
|
+
import tarfile
|
|
10
|
+
import tempfile
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
from contextlib import contextmanager
|
|
7
13
|
from dataclasses import dataclass
|
|
8
14
|
from datetime import UTC, datetime
|
|
9
15
|
from pathlib import Path
|
|
10
16
|
from typing import Any
|
|
17
|
+
from urllib.error import HTTPError, URLError
|
|
18
|
+
from urllib.request import Request, urlopen
|
|
11
19
|
|
|
12
20
|
from .loader import load_yaml_file, resolve_module_dir
|
|
13
21
|
from .planner import MaxNestingDepthExceeded, apply_defaults, substitute_string
|
|
14
22
|
|
|
15
23
|
|
|
24
|
+
# The upstream repository `cds get` downloads from when no `--remote` is
|
|
25
|
+
# given. Keep in sync with the `Repository` URL in pyproject.toml.
|
|
26
|
+
DEFAULT_REMOTE = "RonaldHensbergen/composable-data-stack"
|
|
27
|
+
DEFAULT_REF = "main"
|
|
28
|
+
|
|
29
|
+
_GITHUB_URL_PATTERN = re.compile(
|
|
30
|
+
r"^(?:https?://|git@)?(?:www\.)?github\.com[/:](?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?/?$"
|
|
31
|
+
)
|
|
32
|
+
_GITHUB_SHORTHAND_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
33
|
+
|
|
16
34
|
_TRACKING_FILE = Path(".cds") / "get-manifest.json"
|
|
17
35
|
_SKIP_DIRS = {
|
|
18
36
|
".cds",
|
|
@@ -49,41 +67,46 @@ def fetch_profile(
|
|
|
49
67
|
profile: str,
|
|
50
68
|
*,
|
|
51
69
|
remote: str | None = None,
|
|
70
|
+
ref: str = DEFAULT_REF,
|
|
71
|
+
local: str | None = None,
|
|
52
72
|
destination_root: Path | None = None,
|
|
53
73
|
force: bool = False,
|
|
54
74
|
dry_run: bool = False,
|
|
55
75
|
) -> tuple[list[CopyAction], Path]:
|
|
56
|
-
source_repo = _resolve_source_repository(remote)
|
|
57
76
|
target_root = (destination_root or Path.cwd()).expanduser().resolve()
|
|
58
|
-
profile_path = _resolve_source_profile_path(source_repo, profile)
|
|
59
|
-
asset_roots = _collect_asset_roots(source_repo, profile_path)
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
with _prepare_source_repository(remote, ref, local) as source_repo:
|
|
79
|
+
profile_path = _resolve_source_profile_path(source_repo, profile)
|
|
80
|
+
asset_roots = _collect_asset_roots(source_repo, profile_path)
|
|
64
81
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
extra = "" if len(conflicts) <= 5 else f" (+{len(conflicts) - 5} more)"
|
|
69
|
-
raise GetError(
|
|
70
|
-
"Refusing to overwrite existing files without --force: "
|
|
71
|
-
f"{rendered}{extra}"
|
|
72
|
-
)
|
|
82
|
+
actions = _build_copy_plan(source_repo, asset_roots, target_root)
|
|
83
|
+
if dry_run:
|
|
84
|
+
return actions, target_root / _TRACKING_FILE
|
|
73
85
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
conflicts = _find_conflicts(actions)
|
|
87
|
+
if conflicts and not force:
|
|
88
|
+
rendered = ", ".join(conflicts[:5])
|
|
89
|
+
extra = "" if len(conflicts) <= 5 else f" (+{len(conflicts) - 5} more)"
|
|
90
|
+
raise GetError(
|
|
91
|
+
"Refusing to overwrite existing files without --force: "
|
|
92
|
+
f"{rendered}{extra}"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
for action in actions:
|
|
96
|
+
action.destination.parent.mkdir(parents=True, exist_ok=True)
|
|
97
|
+
shutil.copy2(action.source, action.destination)
|
|
98
|
+
|
|
99
|
+
_write_tracking_manifest(
|
|
100
|
+
target_root=target_root,
|
|
101
|
+
requested_profile=profile,
|
|
102
|
+
source_repo=source_repo,
|
|
103
|
+
profile_path=profile_path,
|
|
104
|
+
remote=remote,
|
|
105
|
+
ref=ref,
|
|
106
|
+
local=local,
|
|
107
|
+
actions=actions,
|
|
108
|
+
asset_roots=asset_roots,
|
|
109
|
+
)
|
|
87
110
|
return actions, target_root / _TRACKING_FILE
|
|
88
111
|
|
|
89
112
|
|
|
@@ -99,8 +122,84 @@ def format_get_plan(actions: list[CopyAction], *, destination_root: Path) -> str
|
|
|
99
122
|
return "\n".join(lines)
|
|
100
123
|
|
|
101
124
|
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
@contextmanager
|
|
126
|
+
def _prepare_source_repository(
|
|
127
|
+
remote: str | None, ref: str, local: str | None
|
|
128
|
+
) -> Iterator[Path]:
|
|
129
|
+
"""Resolve the source repository containing a `profiles/` tree.
|
|
130
|
+
|
|
131
|
+
By design, `cds get` downloads its source from GitHub: a bare `remote`
|
|
132
|
+
defaults to this project's upstream repository, and any `owner/repo` or
|
|
133
|
+
`github.com/...` value is fetched as a tarball for `ref`. Pass `local` to
|
|
134
|
+
explicitly use an existing local directory instead (e.g. an offline/dev
|
|
135
|
+
checkout) -- `remote`/`ref` are ignored in that case.
|
|
136
|
+
"""
|
|
137
|
+
if local is not None:
|
|
138
|
+
if remote is not None:
|
|
139
|
+
raise GetError("Specify only one of --remote and --local")
|
|
140
|
+
yield _validate_source_repository(Path(local).expanduser())
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
candidate = remote or DEFAULT_REMOTE
|
|
144
|
+
parsed = _parse_github_remote(candidate)
|
|
145
|
+
if parsed is None:
|
|
146
|
+
raise GetError(
|
|
147
|
+
f'Could not resolve remote "{candidate}": expected an "owner/repo" '
|
|
148
|
+
'GitHub reference or a github.com URL. Use --local for an existing '
|
|
149
|
+
"local directory instead."
|
|
150
|
+
)
|
|
151
|
+
owner, repo = parsed
|
|
152
|
+
with tempfile.TemporaryDirectory(prefix="cds-get-") as tmp_dir:
|
|
153
|
+
extracted = _download_github_repository(owner, repo, ref, Path(tmp_dir))
|
|
154
|
+
yield _validate_source_repository(extracted)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _parse_github_remote(remote: str) -> tuple[str, str] | None:
|
|
158
|
+
candidate = remote.strip()
|
|
159
|
+
match = _GITHUB_URL_PATTERN.match(candidate)
|
|
160
|
+
if match:
|
|
161
|
+
return match.group("owner"), match.group("repo")
|
|
162
|
+
if _GITHUB_SHORTHAND_PATTERN.match(candidate):
|
|
163
|
+
owner, repo = candidate.split("/", 1)
|
|
164
|
+
return owner, repo
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _download_github_repository(owner: str, repo: str, ref: str, work_dir: Path) -> Path:
|
|
169
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/tarball/{ref}"
|
|
170
|
+
request = Request(url, headers={"User-Agent": "composable-data-stack-cds-get"})
|
|
171
|
+
try:
|
|
172
|
+
with urlopen(request, timeout=30) as response: # nosec B310 - fixed https GitHub API host
|
|
173
|
+
archive_bytes = response.read()
|
|
174
|
+
except HTTPError as exc:
|
|
175
|
+
raise GetError(
|
|
176
|
+
f"Could not download {owner}/{repo}@{ref} from GitHub: HTTP {exc.code}"
|
|
177
|
+
) from exc
|
|
178
|
+
except URLError as exc:
|
|
179
|
+
raise GetError(
|
|
180
|
+
f"Could not download {owner}/{repo}@{ref} from GitHub: {exc.reason}"
|
|
181
|
+
) from exc
|
|
182
|
+
|
|
183
|
+
archive_path = work_dir / "repository.tar.gz"
|
|
184
|
+
archive_path.write_bytes(archive_bytes)
|
|
185
|
+
|
|
186
|
+
extract_root = work_dir / "extracted"
|
|
187
|
+
extract_root.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
try:
|
|
189
|
+
with tarfile.open(archive_path) as archive:
|
|
190
|
+
archive.extractall(extract_root, filter="data")
|
|
191
|
+
except tarfile.TarError as exc:
|
|
192
|
+
raise GetError(
|
|
193
|
+
f"Could not extract archive for {owner}/{repo}@{ref}: {exc}"
|
|
194
|
+
) from exc
|
|
195
|
+
|
|
196
|
+
extracted_entries = [entry for entry in extract_root.iterdir() if entry.is_dir()]
|
|
197
|
+
if len(extracted_entries) != 1:
|
|
198
|
+
raise GetError(f"Unexpected archive layout for {owner}/{repo}@{ref}")
|
|
199
|
+
return extracted_entries[0]
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _validate_source_repository(candidate: Path) -> Path:
|
|
104
203
|
resolved = candidate.resolve()
|
|
105
204
|
if not resolved.exists():
|
|
106
205
|
raise GetError(f"Source repository does not exist: {resolved}")
|
|
@@ -113,14 +212,6 @@ def _resolve_source_repository(remote: str | None) -> Path:
|
|
|
113
212
|
return resolved
|
|
114
213
|
|
|
115
214
|
|
|
116
|
-
def _find_project_root(start: Path | None = None) -> Path:
|
|
117
|
-
current = (start or Path.cwd()).resolve()
|
|
118
|
-
for directory in [current, *current.parents]:
|
|
119
|
-
if (directory / "pyproject.toml").exists() or (directory / ".git").exists():
|
|
120
|
-
return directory
|
|
121
|
-
return current
|
|
122
|
-
|
|
123
|
-
|
|
124
215
|
def _resolve_source_profile_path(source_repo: Path, profile: str) -> Path:
|
|
125
216
|
profile_selector = Path(profile)
|
|
126
217
|
candidates = [
|
|
@@ -544,6 +635,8 @@ def _write_tracking_manifest(
|
|
|
544
635
|
source_repo: Path,
|
|
545
636
|
profile_path: Path,
|
|
546
637
|
remote: str | None,
|
|
638
|
+
ref: str,
|
|
639
|
+
local: str | None,
|
|
547
640
|
actions: list[CopyAction],
|
|
548
641
|
asset_roots: list[Path],
|
|
549
642
|
) -> None:
|
|
@@ -553,7 +646,8 @@ def _write_tracking_manifest(
|
|
|
553
646
|
entry = {
|
|
554
647
|
"requestedProfile": requested_profile,
|
|
555
648
|
"sourceProfile": profile_path.relative_to(source_repo).as_posix(),
|
|
556
|
-
"remote": remote or
|
|
649
|
+
"remote": local or remote or DEFAULT_REMOTE,
|
|
650
|
+
"ref": None if local else ref,
|
|
557
651
|
"fetchedAt": datetime.now(UTC).isoformat(),
|
|
558
652
|
"assetRoots": [
|
|
559
653
|
_asset_root_relative_path(asset_root, source_repo) for asset_root in asset_roots
|
|
@@ -574,15 +668,49 @@ def _write_tracking_manifest(
|
|
|
574
668
|
def _read_tracking_manifest(path: Path) -> dict[str, Any]:
|
|
575
669
|
if not path.exists():
|
|
576
670
|
return {"version": 1, "profiles": {}}
|
|
671
|
+
|
|
577
672
|
try:
|
|
578
|
-
|
|
579
|
-
except
|
|
580
|
-
|
|
581
|
-
|
|
673
|
+
raw_text = path.read_text(encoding="utf-8")
|
|
674
|
+
except OSError as exc:
|
|
675
|
+
# The file could not be read at all, so it cannot be backed up either;
|
|
676
|
+
# skip the doomed copy attempt and report the read failure directly.
|
|
677
|
+
print(
|
|
678
|
+
f"WARNING {path} could not be read ({exc}); resetting tracking manifest.",
|
|
679
|
+
file=sys.stderr,
|
|
680
|
+
)
|
|
582
681
|
return {"version": 1, "profiles": {}}
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
682
|
+
|
|
683
|
+
malformed_reason: str = ""
|
|
684
|
+
try:
|
|
685
|
+
data = json.loads(raw_text)
|
|
686
|
+
except json.JSONDecodeError as exc:
|
|
687
|
+
malformed_reason = f"invalid JSON: {exc}"
|
|
688
|
+
else:
|
|
689
|
+
if not isinstance(data, dict):
|
|
690
|
+
malformed_reason = "manifest root must be a JSON object"
|
|
691
|
+
else:
|
|
692
|
+
data.setdefault("version", 1)
|
|
693
|
+
data.setdefault("profiles", {})
|
|
694
|
+
return data
|
|
695
|
+
|
|
696
|
+
_backup_malformed_manifest(path, malformed_reason)
|
|
697
|
+
return {"version": 1, "profiles": {}}
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _backup_malformed_manifest(path: Path, reason: str) -> None:
|
|
701
|
+
timestamp = datetime.now(UTC).strftime("%Y%m%dT%H%M%S%fZ")
|
|
702
|
+
backup_path = path.with_name(f"{path.name}.corrupt-{timestamp}")
|
|
703
|
+
try:
|
|
704
|
+
shutil.copy2(path, backup_path)
|
|
705
|
+
backup_message = f"backed up to {backup_path}"
|
|
706
|
+
except OSError as exc:
|
|
707
|
+
backup_message = f"backup failed: {exc}"
|
|
708
|
+
|
|
709
|
+
print(
|
|
710
|
+
f"WARNING {path} is malformed ({reason}); resetting tracking manifest "
|
|
711
|
+
f"({backup_message}).",
|
|
712
|
+
file=sys.stderr,
|
|
713
|
+
)
|
|
586
714
|
|
|
587
715
|
|
|
588
716
|
def _asset_root_relative_path(asset_root: Path, source_repo: Path) -> str:
|
|
@@ -114,12 +114,19 @@ def get_modules_root() -> Path:
|
|
|
114
114
|
def find_project_root(start: Path | None = None) -> Path:
|
|
115
115
|
"""
|
|
116
116
|
Walk up from `start` (default: current working directory) looking for a
|
|
117
|
-
project root marker (
|
|
118
|
-
|
|
117
|
+
project root marker: `.cds` (CDS's own state directory, created by
|
|
118
|
+
`cds get`/`cds use`), `pyproject.toml`, or `.git`. `.cds` is checked first
|
|
119
|
+
at each level so a CDS working directory takes priority over an unrelated
|
|
120
|
+
ancestor repository (e.g. a dotfiles repo at $HOME) that happens to sit
|
|
121
|
+
further up the tree. Falls back to `start` itself if no marker is found.
|
|
119
122
|
"""
|
|
120
123
|
current = (start or Path.cwd()).resolve()
|
|
121
124
|
for directory in [current, *current.parents]:
|
|
122
|
-
if (
|
|
125
|
+
if (
|
|
126
|
+
(directory / ".cds").exists()
|
|
127
|
+
or (directory / "pyproject.toml").exists()
|
|
128
|
+
or (directory / ".git").exists()
|
|
129
|
+
):
|
|
123
130
|
return directory
|
|
124
131
|
return current
|
|
125
132
|
|
|
@@ -335,13 +342,19 @@ def resolve_project_root(profile_path: str) -> Path:
|
|
|
335
342
|
"""
|
|
336
343
|
Resolve a project root for output artifacts.
|
|
337
344
|
|
|
338
|
-
The resolver walks up from the selected profile location and picks the
|
|
339
|
-
directory containing
|
|
340
|
-
it
|
|
345
|
+
The resolver walks up from the selected profile location and picks the
|
|
346
|
+
first directory containing `.cds` (CDS's own state directory, checked
|
|
347
|
+
first so it takes priority over an unrelated ancestor repository),
|
|
348
|
+
`pyproject.toml`, or `.git`. If no marker is found, it falls back to the
|
|
349
|
+
current working directory.
|
|
341
350
|
"""
|
|
342
351
|
start = Path(profile_path).resolve().parent
|
|
343
352
|
for directory in [start, *start.parents]:
|
|
344
|
-
if (
|
|
353
|
+
if (
|
|
354
|
+
(directory / ".cds").exists()
|
|
355
|
+
or (directory / "pyproject.toml").exists()
|
|
356
|
+
or (directory / ".git").exists()
|
|
357
|
+
):
|
|
345
358
|
return directory
|
|
346
359
|
return Path.cwd().resolve()
|
|
347
360
|
|
|
@@ -853,7 +866,22 @@ def main() -> int:
|
|
|
853
866
|
)
|
|
854
867
|
get_parser.add_argument(
|
|
855
868
|
"--remote",
|
|
856
|
-
help=
|
|
869
|
+
help=(
|
|
870
|
+
"GitHub repository to fetch from, as 'owner/repo' or a github.com URL "
|
|
871
|
+
"(default: the upstream composable-data-stack repository)"
|
|
872
|
+
),
|
|
873
|
+
)
|
|
874
|
+
get_parser.add_argument(
|
|
875
|
+
"--ref",
|
|
876
|
+
default="main",
|
|
877
|
+
help="Branch, tag, or commit to fetch from --remote (default: main)",
|
|
878
|
+
)
|
|
879
|
+
get_parser.add_argument(
|
|
880
|
+
"--local",
|
|
881
|
+
help=(
|
|
882
|
+
"Use an existing local directory as the source repository instead of "
|
|
883
|
+
"downloading from GitHub (mutually exclusive with --remote/--ref)"
|
|
884
|
+
),
|
|
857
885
|
)
|
|
858
886
|
get_parser.add_argument(
|
|
859
887
|
"--into",
|
|
@@ -1279,6 +1307,8 @@ def main() -> int:
|
|
|
1279
1307
|
actions, manifest_path = fetch_profile(
|
|
1280
1308
|
args.profile,
|
|
1281
1309
|
remote=args.remote,
|
|
1310
|
+
ref=args.ref,
|
|
1311
|
+
local=args.local,
|
|
1282
1312
|
destination_root=Path(args.into) if args.into else None,
|
|
1283
1313
|
force=args.force,
|
|
1284
1314
|
dry_run=args.dry_run,
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.2/composable_data_stack.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: composable-data-stack
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: CLI for validating and composing composable data stack profiles
|
|
5
5
|
Author: Ronald Hensbergen
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -55,6 +55,10 @@ Composable Data Stack (CDS) is a framework for defining and assembling data plat
|
|
|
55
55
|
|
|
56
56
|
> **Note:** Development helper tools are located in the `tools/` directory (git-ignored). See `tools/pr-cli/README.md` for PR creation scripts.
|
|
57
57
|
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## ⚡ Why CDS
|
|
61
|
+
|
|
58
62
|
Instead of hardcoding integrations or relying on fragile pipelines, CDS introduces:
|
|
59
63
|
|
|
60
64
|
- 🔧 **Modules**: reusable components (Dagster, Postgres, Superset)
|
|
@@ -63,10 +67,6 @@ Instead of hardcoding integrations or relying on fragile pipelines, CDS introduc
|
|
|
63
67
|
|
|
64
68
|
Think of it as Infrastructure as Code, but for data platforms.
|
|
65
69
|
|
|
66
|
-
---
|
|
67
|
-
|
|
68
|
-
## ⚡ Why CDS
|
|
69
|
-
|
|
70
70
|
Modern data platforms force a trade-off:
|
|
71
71
|
|
|
72
72
|
|Approach|Problem|
|
|
@@ -642,7 +642,7 @@ before and never look for an `environments/` directory.
|
|
|
642
642
|
|
|
643
643
|
|Command|Description|
|
|
644
644
|
|---|---|
|
|
645
|
-
|cds get \<profile\> [--remote \<repo\>] [--into \<dir\>]|Fetch a profile plus its dependent module/runtime assets into a local CDS layout|
|
|
645
|
+
|cds get \<profile\> [--remote \<owner/repo\>] [--ref \<ref\>] [--local \<dir\>] [--into \<dir\>]|Fetch a profile plus its dependent module/runtime assets from GitHub into a local CDS layout|
|
|
646
646
|
|cds init [profile]|Generate a project `.env` template from profile secret definitions|
|
|
647
647
|
|cds validate [profile]|Validate modules and contracts|
|
|
648
648
|
|cds preflight [profile]|Check runtime tools, required environment values, and host ports without starting services|
|
|
@@ -663,12 +663,16 @@ all accept `--environment <name>` (or `-e <name>`) to merge
|
|
|
663
663
|
|
|
664
664
|
`cds get` copies the selected `profiles/<name>/` tree, every referenced module
|
|
665
665
|
directory, and any local build-context assets referenced by those modules'
|
|
666
|
-
Dockerfiles. By
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
666
|
+
Dockerfiles. By design it downloads from GitHub rather than a local checkout:
|
|
667
|
+
by default it fetches this project's upstream repository at the `main` branch,
|
|
668
|
+
downloading a tarball via the GitHub API (no `git` binary required). Use
|
|
669
|
+
`--remote <owner/repo>` (or a `github.com/...` URL) to fetch a fork, and
|
|
670
|
+
`--ref <branch|tag|sha>` to select a specific revision. Pass `--local <dir>`
|
|
671
|
+
to use an existing local directory instead of downloading (mutually exclusive
|
|
672
|
+
with `--remote`/`--ref`) for offline/dev workflows. Use `--into` to choose a
|
|
673
|
+
destination root, `--dry-run` to inspect the copy plan first, and `--force` to
|
|
674
|
+
replace conflicting local files. Successful fetches record tracking metadata
|
|
675
|
+
in `.cds/get-manifest.json` for future update workflows.
|
|
672
676
|
|
|
673
677
|
`[profile]` accepts:
|
|
674
678
|
|