composable-data-stack 0.5.0__tar.gz → 0.5.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.
- {composable_data_stack-0.5.0/composable_data_stack.egg-info → composable_data_stack-0.5.1}/PKG-INFO +16 -12
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/README.md +15 -11
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/getter.py +131 -38
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/main.py +18 -1
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1/composable_data_stack.egg-info}/PKG-INFO +16 -12
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/pyproject.toml +1 -1
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_getter.py +151 -16
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_main.py +2 -2
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/LICENSE +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/__init__.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/constants.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/diagnostics.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/graph.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/image_updates.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/image_verification.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/loader.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/overlay.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/planner.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/preflight.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/renderer.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resolver.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/__init__.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/contract.schema.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/module.schema.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/profile.schema.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/rule-schema.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/rule-set.json +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/secrets.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/security.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/security_common.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/state.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/up_runner.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/utils.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/validator.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/composable_data_stack.egg-info/SOURCES.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/composable_data_stack.egg-info/dependency_links.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/composable_data_stack.egg-info/entry_points.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/composable_data_stack.egg-info/requires.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/composable_data_stack.egg-info/top_level.txt +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/setup.cfg +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_cds_workflow.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_completion.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_compose_runtime_smoke.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_dagster_config_generation.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_dagster_definitions_uri.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_dagster_entrypoint_backend_guard.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_dagster_hardening.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_environment_classification.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_environment_cli.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_graph.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_image_security_scan_workflow.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_image_updates.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_image_verification.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_module_isolation.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_modules_no_committed_secrets.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_mvp_proof_workflow.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_overlay.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_planner.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_preflight.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_publish_images_workflow.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_python_version_alignment.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_release_version_check.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_render_example_profile.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_renderer.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_resolve_profile_path.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_resolver.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_secrets.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_security.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_smoke_example_profile.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_state.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_superset_hardening.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_trivyignore.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_up_runner.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_upstream_image_hardening.py +0 -0
- {composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_validator.py +0 -0
{composable_data_stack-0.5.0/composable_data_stack.egg-info → composable_data_stack-0.5.1}/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.1
|
|
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,34 @@ 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 tarfile
|
|
9
|
+
import tempfile
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from contextlib import contextmanager
|
|
7
12
|
from dataclasses import dataclass
|
|
8
13
|
from datetime import UTC, datetime
|
|
9
14
|
from pathlib import Path
|
|
10
15
|
from typing import Any
|
|
16
|
+
from urllib.error import HTTPError, URLError
|
|
17
|
+
from urllib.request import Request, urlopen
|
|
11
18
|
|
|
12
19
|
from .loader import load_yaml_file, resolve_module_dir
|
|
13
20
|
from .planner import MaxNestingDepthExceeded, apply_defaults, substitute_string
|
|
14
21
|
|
|
15
22
|
|
|
23
|
+
# The upstream repository `cds get` downloads from when no `--remote` is
|
|
24
|
+
# given. Keep in sync with the `Repository` URL in pyproject.toml.
|
|
25
|
+
DEFAULT_REMOTE = "RonaldHensbergen/composable-data-stack"
|
|
26
|
+
DEFAULT_REF = "main"
|
|
27
|
+
|
|
28
|
+
_GITHUB_URL_PATTERN = re.compile(
|
|
29
|
+
r"^(?:https?://|git@)?(?:www\.)?github\.com[/:](?P<owner>[^/]+)/(?P<repo>[^/]+?)(?:\.git)?/?$"
|
|
30
|
+
)
|
|
31
|
+
_GITHUB_SHORTHAND_PATTERN = re.compile(r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$")
|
|
32
|
+
|
|
16
33
|
_TRACKING_FILE = Path(".cds") / "get-manifest.json"
|
|
17
34
|
_SKIP_DIRS = {
|
|
18
35
|
".cds",
|
|
@@ -49,41 +66,46 @@ def fetch_profile(
|
|
|
49
66
|
profile: str,
|
|
50
67
|
*,
|
|
51
68
|
remote: str | None = None,
|
|
69
|
+
ref: str = DEFAULT_REF,
|
|
70
|
+
local: str | None = None,
|
|
52
71
|
destination_root: Path | None = None,
|
|
53
72
|
force: bool = False,
|
|
54
73
|
dry_run: bool = False,
|
|
55
74
|
) -> tuple[list[CopyAction], Path]:
|
|
56
|
-
source_repo = _resolve_source_repository(remote)
|
|
57
75
|
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
76
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
with _prepare_source_repository(remote, ref, local) as source_repo:
|
|
78
|
+
profile_path = _resolve_source_profile_path(source_repo, profile)
|
|
79
|
+
asset_roots = _collect_asset_roots(source_repo, profile_path)
|
|
64
80
|
|
|
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
|
-
)
|
|
81
|
+
actions = _build_copy_plan(source_repo, asset_roots, target_root)
|
|
82
|
+
if dry_run:
|
|
83
|
+
return actions, target_root / _TRACKING_FILE
|
|
73
84
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
conflicts = _find_conflicts(actions)
|
|
86
|
+
if conflicts and not force:
|
|
87
|
+
rendered = ", ".join(conflicts[:5])
|
|
88
|
+
extra = "" if len(conflicts) <= 5 else f" (+{len(conflicts) - 5} more)"
|
|
89
|
+
raise GetError(
|
|
90
|
+
"Refusing to overwrite existing files without --force: "
|
|
91
|
+
f"{rendered}{extra}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
for action in actions:
|
|
95
|
+
action.destination.parent.mkdir(parents=True, exist_ok=True)
|
|
96
|
+
shutil.copy2(action.source, action.destination)
|
|
97
|
+
|
|
98
|
+
_write_tracking_manifest(
|
|
99
|
+
target_root=target_root,
|
|
100
|
+
requested_profile=profile,
|
|
101
|
+
source_repo=source_repo,
|
|
102
|
+
profile_path=profile_path,
|
|
103
|
+
remote=remote,
|
|
104
|
+
ref=ref,
|
|
105
|
+
local=local,
|
|
106
|
+
actions=actions,
|
|
107
|
+
asset_roots=asset_roots,
|
|
108
|
+
)
|
|
87
109
|
return actions, target_root / _TRACKING_FILE
|
|
88
110
|
|
|
89
111
|
|
|
@@ -99,8 +121,84 @@ def format_get_plan(actions: list[CopyAction], *, destination_root: Path) -> str
|
|
|
99
121
|
return "\n".join(lines)
|
|
100
122
|
|
|
101
123
|
|
|
102
|
-
|
|
103
|
-
|
|
124
|
+
@contextmanager
|
|
125
|
+
def _prepare_source_repository(
|
|
126
|
+
remote: str | None, ref: str, local: str | None
|
|
127
|
+
) -> Iterator[Path]:
|
|
128
|
+
"""Resolve the source repository containing a `profiles/` tree.
|
|
129
|
+
|
|
130
|
+
By design, `cds get` downloads its source from GitHub: a bare `remote`
|
|
131
|
+
defaults to this project's upstream repository, and any `owner/repo` or
|
|
132
|
+
`github.com/...` value is fetched as a tarball for `ref`. Pass `local` to
|
|
133
|
+
explicitly use an existing local directory instead (e.g. an offline/dev
|
|
134
|
+
checkout) -- `remote`/`ref` are ignored in that case.
|
|
135
|
+
"""
|
|
136
|
+
if local is not None:
|
|
137
|
+
if remote is not None:
|
|
138
|
+
raise GetError("Specify only one of --remote and --local")
|
|
139
|
+
yield _validate_source_repository(Path(local).expanduser())
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
candidate = remote or DEFAULT_REMOTE
|
|
143
|
+
parsed = _parse_github_remote(candidate)
|
|
144
|
+
if parsed is None:
|
|
145
|
+
raise GetError(
|
|
146
|
+
f'Could not resolve remote "{candidate}": expected an "owner/repo" '
|
|
147
|
+
'GitHub reference or a github.com URL. Use --local for an existing '
|
|
148
|
+
"local directory instead."
|
|
149
|
+
)
|
|
150
|
+
owner, repo = parsed
|
|
151
|
+
with tempfile.TemporaryDirectory(prefix="cds-get-") as tmp_dir:
|
|
152
|
+
extracted = _download_github_repository(owner, repo, ref, Path(tmp_dir))
|
|
153
|
+
yield _validate_source_repository(extracted)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _parse_github_remote(remote: str) -> tuple[str, str] | None:
|
|
157
|
+
candidate = remote.strip()
|
|
158
|
+
match = _GITHUB_URL_PATTERN.match(candidate)
|
|
159
|
+
if match:
|
|
160
|
+
return match.group("owner"), match.group("repo")
|
|
161
|
+
if _GITHUB_SHORTHAND_PATTERN.match(candidate):
|
|
162
|
+
owner, repo = candidate.split("/", 1)
|
|
163
|
+
return owner, repo
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _download_github_repository(owner: str, repo: str, ref: str, work_dir: Path) -> Path:
|
|
168
|
+
url = f"https://api.github.com/repos/{owner}/{repo}/tarball/{ref}"
|
|
169
|
+
request = Request(url, headers={"User-Agent": "composable-data-stack-cds-get"})
|
|
170
|
+
try:
|
|
171
|
+
with urlopen(request, timeout=30) as response: # nosec B310 - fixed https GitHub API host
|
|
172
|
+
archive_bytes = response.read()
|
|
173
|
+
except HTTPError as exc:
|
|
174
|
+
raise GetError(
|
|
175
|
+
f"Could not download {owner}/{repo}@{ref} from GitHub: HTTP {exc.code}"
|
|
176
|
+
) from exc
|
|
177
|
+
except URLError as exc:
|
|
178
|
+
raise GetError(
|
|
179
|
+
f"Could not download {owner}/{repo}@{ref} from GitHub: {exc.reason}"
|
|
180
|
+
) from exc
|
|
181
|
+
|
|
182
|
+
archive_path = work_dir / "repository.tar.gz"
|
|
183
|
+
archive_path.write_bytes(archive_bytes)
|
|
184
|
+
|
|
185
|
+
extract_root = work_dir / "extracted"
|
|
186
|
+
extract_root.mkdir(parents=True, exist_ok=True)
|
|
187
|
+
try:
|
|
188
|
+
with tarfile.open(archive_path) as archive:
|
|
189
|
+
archive.extractall(extract_root, filter="data")
|
|
190
|
+
except tarfile.TarError as exc:
|
|
191
|
+
raise GetError(
|
|
192
|
+
f"Could not extract archive for {owner}/{repo}@{ref}: {exc}"
|
|
193
|
+
) from exc
|
|
194
|
+
|
|
195
|
+
extracted_entries = [entry for entry in extract_root.iterdir() if entry.is_dir()]
|
|
196
|
+
if len(extracted_entries) != 1:
|
|
197
|
+
raise GetError(f"Unexpected archive layout for {owner}/{repo}@{ref}")
|
|
198
|
+
return extracted_entries[0]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _validate_source_repository(candidate: Path) -> Path:
|
|
104
202
|
resolved = candidate.resolve()
|
|
105
203
|
if not resolved.exists():
|
|
106
204
|
raise GetError(f"Source repository does not exist: {resolved}")
|
|
@@ -113,14 +211,6 @@ def _resolve_source_repository(remote: str | None) -> Path:
|
|
|
113
211
|
return resolved
|
|
114
212
|
|
|
115
213
|
|
|
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
214
|
def _resolve_source_profile_path(source_repo: Path, profile: str) -> Path:
|
|
125
215
|
profile_selector = Path(profile)
|
|
126
216
|
candidates = [
|
|
@@ -544,6 +634,8 @@ def _write_tracking_manifest(
|
|
|
544
634
|
source_repo: Path,
|
|
545
635
|
profile_path: Path,
|
|
546
636
|
remote: str | None,
|
|
637
|
+
ref: str,
|
|
638
|
+
local: str | None,
|
|
547
639
|
actions: list[CopyAction],
|
|
548
640
|
asset_roots: list[Path],
|
|
549
641
|
) -> None:
|
|
@@ -553,7 +645,8 @@ def _write_tracking_manifest(
|
|
|
553
645
|
entry = {
|
|
554
646
|
"requestedProfile": requested_profile,
|
|
555
647
|
"sourceProfile": profile_path.relative_to(source_repo).as_posix(),
|
|
556
|
-
"remote": remote or
|
|
648
|
+
"remote": local or remote or DEFAULT_REMOTE,
|
|
649
|
+
"ref": None if local else ref,
|
|
557
650
|
"fetchedAt": datetime.now(UTC).isoformat(),
|
|
558
651
|
"assetRoots": [
|
|
559
652
|
_asset_root_relative_path(asset_root, source_repo) for asset_root in asset_roots
|
|
@@ -853,7 +853,22 @@ def main() -> int:
|
|
|
853
853
|
)
|
|
854
854
|
get_parser.add_argument(
|
|
855
855
|
"--remote",
|
|
856
|
-
help=
|
|
856
|
+
help=(
|
|
857
|
+
"GitHub repository to fetch from, as 'owner/repo' or a github.com URL "
|
|
858
|
+
"(default: the upstream composable-data-stack repository)"
|
|
859
|
+
),
|
|
860
|
+
)
|
|
861
|
+
get_parser.add_argument(
|
|
862
|
+
"--ref",
|
|
863
|
+
default="main",
|
|
864
|
+
help="Branch, tag, or commit to fetch from --remote (default: main)",
|
|
865
|
+
)
|
|
866
|
+
get_parser.add_argument(
|
|
867
|
+
"--local",
|
|
868
|
+
help=(
|
|
869
|
+
"Use an existing local directory as the source repository instead of "
|
|
870
|
+
"downloading from GitHub (mutually exclusive with --remote/--ref)"
|
|
871
|
+
),
|
|
857
872
|
)
|
|
858
873
|
get_parser.add_argument(
|
|
859
874
|
"--into",
|
|
@@ -1279,6 +1294,8 @@ def main() -> int:
|
|
|
1279
1294
|
actions, manifest_path = fetch_profile(
|
|
1280
1295
|
args.profile,
|
|
1281
1296
|
remote=args.remote,
|
|
1297
|
+
ref=args.ref,
|
|
1298
|
+
local=args.local,
|
|
1282
1299
|
destination_root=Path(args.into) if args.into else None,
|
|
1283
1300
|
force=args.force,
|
|
1284
1301
|
dry_run=args.dry_run,
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1/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.1
|
|
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
|
|
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import io
|
|
1
2
|
import json
|
|
2
3
|
import os
|
|
3
4
|
import stat
|
|
4
5
|
import sys
|
|
6
|
+
import tarfile
|
|
5
7
|
import tempfile
|
|
6
8
|
import unittest
|
|
7
9
|
from pathlib import Path
|
|
10
|
+
from unittest.mock import patch
|
|
8
11
|
|
|
9
|
-
from cli.getter import GetError, fetch_profile
|
|
12
|
+
from cli.getter import DEFAULT_REMOTE, GetError, _parse_github_remote, fetch_profile
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
def _write(path: Path, content: str) -> None:
|
|
@@ -74,7 +77,7 @@ class GetterTest(unittest.TestCase):
|
|
|
74
77
|
|
|
75
78
|
actions, manifest_path = fetch_profile(
|
|
76
79
|
"demo",
|
|
77
|
-
|
|
80
|
+
local=str(source_root),
|
|
78
81
|
destination_root=destination_root,
|
|
79
82
|
)
|
|
80
83
|
|
|
@@ -101,16 +104,16 @@ class GetterTest(unittest.TestCase):
|
|
|
101
104
|
destination_root = Path(dest_dir)
|
|
102
105
|
_make_source_repo(source_root)
|
|
103
106
|
|
|
104
|
-
fetch_profile("demo",
|
|
107
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
105
108
|
profile_file = destination_root / "profiles" / "demo" / "profile.yaml"
|
|
106
109
|
profile_file.write_text("changed\n", encoding="utf-8")
|
|
107
110
|
|
|
108
111
|
with self.assertRaises(GetError):
|
|
109
|
-
fetch_profile("demo",
|
|
112
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
110
113
|
|
|
111
114
|
fetch_profile(
|
|
112
115
|
"demo",
|
|
113
|
-
|
|
116
|
+
local=str(source_root),
|
|
114
117
|
destination_root=destination_root,
|
|
115
118
|
force=True,
|
|
116
119
|
)
|
|
@@ -122,13 +125,13 @@ class GetterTest(unittest.TestCase):
|
|
|
122
125
|
destination_root = Path(dest_dir)
|
|
123
126
|
_make_source_repo(source_root)
|
|
124
127
|
|
|
125
|
-
fetch_profile("demo",
|
|
128
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
126
129
|
profile_file = destination_root / "profiles" / "demo" / "profile.yaml"
|
|
127
130
|
profile_file.write_text("changed\n", encoding="utf-8")
|
|
128
131
|
|
|
129
132
|
actions, manifest_path = fetch_profile(
|
|
130
133
|
"demo",
|
|
131
|
-
|
|
134
|
+
local=str(source_root),
|
|
132
135
|
destination_root=destination_root,
|
|
133
136
|
dry_run=True,
|
|
134
137
|
)
|
|
@@ -198,7 +201,7 @@ COPY shared/python /app/shared/python
|
|
|
198
201
|
_write(source_root / ".github" / "workflows" / "ci.yml", "name: CI\n")
|
|
199
202
|
_write(source_root / ".env.example", "EXAMPLE=true\n")
|
|
200
203
|
|
|
201
|
-
fetch_profile("demo",
|
|
204
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
202
205
|
|
|
203
206
|
self.assertTrue(
|
|
204
207
|
(destination_root / "images" / "demo" / "hardened" / "Dockerfile").exists()
|
|
@@ -243,7 +246,7 @@ spec:
|
|
|
243
246
|
|
|
244
247
|
_, manifest_path = fetch_profile(
|
|
245
248
|
"demo",
|
|
246
|
-
|
|
249
|
+
local=str(source_root),
|
|
247
250
|
destination_root=destination_root,
|
|
248
251
|
)
|
|
249
252
|
|
|
@@ -294,7 +297,7 @@ COPY ["shared/python", dest]
|
|
|
294
297
|
)
|
|
295
298
|
|
|
296
299
|
with self.assertRaises(GetError) as ctx:
|
|
297
|
-
fetch_profile("demo",
|
|
300
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
298
301
|
|
|
299
302
|
self.assertIn("Could not parse COPY sources", str(ctx.exception))
|
|
300
303
|
|
|
@@ -341,7 +344,7 @@ COPY "shared/python /app/
|
|
|
341
344
|
)
|
|
342
345
|
|
|
343
346
|
with self.assertRaises(GetError) as ctx:
|
|
344
|
-
fetch_profile("demo",
|
|
347
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
345
348
|
|
|
346
349
|
self.assertIn("Could not parse COPY sources", str(ctx.exception))
|
|
347
350
|
|
|
@@ -388,7 +391,7 @@ COPY "shared/file#1.txt" /app/
|
|
|
388
391
|
)
|
|
389
392
|
_write(source_root / "shared" / "file#1.txt", "ok\n")
|
|
390
393
|
|
|
391
|
-
fetch_profile("demo",
|
|
394
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
392
395
|
|
|
393
396
|
self.assertTrue((destination_root / "shared" / "file#1.txt").exists())
|
|
394
397
|
|
|
@@ -438,7 +441,7 @@ COPY shared/python /app/shared/python
|
|
|
438
441
|
)
|
|
439
442
|
_write(source_root / "shared" / "python" / "__init__.py", "")
|
|
440
443
|
|
|
441
|
-
fetch_profile("demo",
|
|
444
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
442
445
|
|
|
443
446
|
self.assertTrue((destination_root / "images" / "demo" / "Dockerfile").exists())
|
|
444
447
|
self.assertTrue((destination_root / "shared" / "python" / "__init__.py").exists())
|
|
@@ -466,7 +469,7 @@ spec:
|
|
|
466
469
|
with self.assertRaises(GetError) as ctx:
|
|
467
470
|
fetch_profile(
|
|
468
471
|
f"../{outside_root.name}/evil.yaml",
|
|
469
|
-
|
|
472
|
+
local=str(source_root),
|
|
470
473
|
destination_root=destination_root,
|
|
471
474
|
)
|
|
472
475
|
|
|
@@ -509,7 +512,7 @@ spec:
|
|
|
509
512
|
)
|
|
510
513
|
|
|
511
514
|
with self.assertRaises(GetError) as ctx:
|
|
512
|
-
fetch_profile("demo",
|
|
515
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
513
516
|
|
|
514
517
|
self.assertIn('build.dockerfile "/etc/passwd" resolves outside the source repository', str(ctx.exception))
|
|
515
518
|
|
|
@@ -523,12 +526,144 @@ spec:
|
|
|
523
526
|
entrypoint = source_root / "images" / "demo" / "entrypoint.sh"
|
|
524
527
|
os.chmod(entrypoint, 0o755)
|
|
525
528
|
|
|
526
|
-
fetch_profile("demo",
|
|
529
|
+
fetch_profile("demo", local=str(source_root), destination_root=destination_root)
|
|
527
530
|
|
|
528
531
|
destination_entrypoint = destination_root / "images" / "demo" / "entrypoint.sh"
|
|
529
532
|
mode = stat.S_IMODE(destination_entrypoint.stat().st_mode)
|
|
530
533
|
self.assertEqual(mode, 0o755)
|
|
531
534
|
|
|
532
535
|
|
|
536
|
+
class GitHubRemoteTest(unittest.TestCase):
|
|
537
|
+
def _make_tarball(self, root: Path) -> bytes:
|
|
538
|
+
buffer = io.BytesIO()
|
|
539
|
+
with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
|
|
540
|
+
archive.add(root, arcname="owner-demo-repo-abcdef1")
|
|
541
|
+
return buffer.getvalue()
|
|
542
|
+
|
|
543
|
+
def test_parse_github_remote_accepts_shorthand_and_urls(self) -> None:
|
|
544
|
+
self.assertEqual(_parse_github_remote("owner/repo"), ("owner", "repo"))
|
|
545
|
+
self.assertEqual(
|
|
546
|
+
_parse_github_remote("https://github.com/owner/repo"), ("owner", "repo")
|
|
547
|
+
)
|
|
548
|
+
self.assertEqual(
|
|
549
|
+
_parse_github_remote("https://github.com/owner/repo.git"), ("owner", "repo")
|
|
550
|
+
)
|
|
551
|
+
self.assertEqual(
|
|
552
|
+
_parse_github_remote("git@github.com:owner/repo.git"), ("owner", "repo")
|
|
553
|
+
)
|
|
554
|
+
self.assertIsNone(_parse_github_remote("not a remote at all"))
|
|
555
|
+
|
|
556
|
+
def test_fetch_profile_downloads_default_remote_when_no_remote_given(self) -> None:
|
|
557
|
+
with tempfile.TemporaryDirectory() as source_dir, tempfile.TemporaryDirectory() as dest_dir:
|
|
558
|
+
source_root = Path(source_dir)
|
|
559
|
+
destination_root = Path(dest_dir)
|
|
560
|
+
_make_source_repo(source_root)
|
|
561
|
+
archive_bytes = self._make_tarball(source_root)
|
|
562
|
+
|
|
563
|
+
class _FakeResponse:
|
|
564
|
+
def __enter__(self_inner):
|
|
565
|
+
return self_inner
|
|
566
|
+
|
|
567
|
+
def __exit__(self_inner, *exc_info):
|
|
568
|
+
return False
|
|
569
|
+
|
|
570
|
+
def read(self_inner):
|
|
571
|
+
return archive_bytes
|
|
572
|
+
|
|
573
|
+
captured_urls: list[str] = []
|
|
574
|
+
|
|
575
|
+
def _fake_urlopen(request, timeout=30):
|
|
576
|
+
captured_urls.append(request.full_url)
|
|
577
|
+
return _FakeResponse()
|
|
578
|
+
|
|
579
|
+
with patch("cli.getter.urlopen", side_effect=_fake_urlopen):
|
|
580
|
+
actions, manifest_path = fetch_profile(
|
|
581
|
+
"demo",
|
|
582
|
+
destination_root=destination_root,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
self.assertEqual(
|
|
586
|
+
captured_urls,
|
|
587
|
+
[f"https://api.github.com/repos/{DEFAULT_REMOTE}/tarball/main"],
|
|
588
|
+
)
|
|
589
|
+
self.assertGreater(len(actions), 0)
|
|
590
|
+
self.assertTrue((destination_root / "profiles" / "demo" / "profile.yaml").exists())
|
|
591
|
+
|
|
592
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
593
|
+
entry = manifest["profiles"]["demo"]
|
|
594
|
+
self.assertEqual(entry["remote"], DEFAULT_REMOTE)
|
|
595
|
+
self.assertEqual(entry["ref"], "main")
|
|
596
|
+
|
|
597
|
+
def test_fetch_profile_downloads_explicit_owner_repo_and_ref(self) -> None:
|
|
598
|
+
with tempfile.TemporaryDirectory() as source_dir, tempfile.TemporaryDirectory() as dest_dir:
|
|
599
|
+
source_root = Path(source_dir)
|
|
600
|
+
destination_root = Path(dest_dir)
|
|
601
|
+
_make_source_repo(source_root)
|
|
602
|
+
archive_bytes = self._make_tarball(source_root)
|
|
603
|
+
|
|
604
|
+
class _FakeResponse:
|
|
605
|
+
def __enter__(self_inner):
|
|
606
|
+
return self_inner
|
|
607
|
+
|
|
608
|
+
def __exit__(self_inner, *exc_info):
|
|
609
|
+
return False
|
|
610
|
+
|
|
611
|
+
def read(self_inner):
|
|
612
|
+
return archive_bytes
|
|
613
|
+
|
|
614
|
+
def _fake_urlopen(request, timeout=30):
|
|
615
|
+
return _FakeResponse()
|
|
616
|
+
|
|
617
|
+
with patch("cli.getter.urlopen", side_effect=_fake_urlopen):
|
|
618
|
+
actions, _ = fetch_profile(
|
|
619
|
+
"demo",
|
|
620
|
+
remote="RonaldHensbergen/composable-data-stack",
|
|
621
|
+
ref="v1.2.3",
|
|
622
|
+
destination_root=destination_root,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
self.assertGreater(len(actions), 0)
|
|
626
|
+
self.assertTrue((destination_root / "modules" / "apps" / "demo" / "module.yaml").exists())
|
|
627
|
+
|
|
628
|
+
def test_fetch_profile_raises_get_error_on_download_failure(self) -> None:
|
|
629
|
+
from urllib.error import URLError
|
|
630
|
+
|
|
631
|
+
def _fake_urlopen(request, timeout=30):
|
|
632
|
+
raise URLError("network unreachable")
|
|
633
|
+
|
|
634
|
+
with tempfile.TemporaryDirectory() as dest_dir:
|
|
635
|
+
with patch("cli.getter.urlopen", side_effect=_fake_urlopen):
|
|
636
|
+
with self.assertRaises(GetError) as ctx:
|
|
637
|
+
fetch_profile("demo", destination_root=Path(dest_dir))
|
|
638
|
+
|
|
639
|
+
self.assertIn("Could not download", str(ctx.exception))
|
|
640
|
+
|
|
641
|
+
def test_fetch_profile_rejects_unresolvable_remote(self) -> None:
|
|
642
|
+
with tempfile.TemporaryDirectory() as dest_dir:
|
|
643
|
+
with self.assertRaises(GetError) as ctx:
|
|
644
|
+
fetch_profile(
|
|
645
|
+
"demo",
|
|
646
|
+
remote="this is not a remote",
|
|
647
|
+
destination_root=Path(dest_dir),
|
|
648
|
+
)
|
|
649
|
+
self.assertIn("Could not resolve remote", str(ctx.exception))
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def test_fetch_profile_rejects_both_remote_and_local(self) -> None:
|
|
653
|
+
with tempfile.TemporaryDirectory() as source_dir, tempfile.TemporaryDirectory() as dest_dir:
|
|
654
|
+
source_root = Path(source_dir)
|
|
655
|
+
destination_root = Path(dest_dir)
|
|
656
|
+
_make_source_repo(source_root)
|
|
657
|
+
|
|
658
|
+
with self.assertRaises(GetError) as ctx:
|
|
659
|
+
fetch_profile(
|
|
660
|
+
"demo",
|
|
661
|
+
remote="owner/repo",
|
|
662
|
+
local=str(source_root),
|
|
663
|
+
destination_root=destination_root,
|
|
664
|
+
)
|
|
665
|
+
self.assertIn("Specify only one of --remote and --local", str(ctx.exception))
|
|
666
|
+
|
|
667
|
+
|
|
533
668
|
if __name__ == "__main__":
|
|
534
669
|
unittest.main()
|
|
@@ -475,7 +475,7 @@ spec:
|
|
|
475
475
|
"cds",
|
|
476
476
|
"get",
|
|
477
477
|
"demo",
|
|
478
|
-
"--
|
|
478
|
+
"--local",
|
|
479
479
|
str(source_root),
|
|
480
480
|
"--into",
|
|
481
481
|
str(destination_root),
|
|
@@ -500,7 +500,7 @@ spec:
|
|
|
500
500
|
"cds",
|
|
501
501
|
"get",
|
|
502
502
|
"missing-profile",
|
|
503
|
-
"--
|
|
503
|
+
"--local",
|
|
504
504
|
str(Path(tempfile.gettempdir()) / "cds-missing-source-repo"),
|
|
505
505
|
],
|
|
506
506
|
), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/contract.schema.json
RENAMED
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/module.schema.json
RENAMED
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/cli/resources/profile.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_compose_runtime_smoke.py
RENAMED
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_dagster_config_generation.py
RENAMED
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_dagster_definitions_uri.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_environment_classification.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_image_verification.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_mvp_proof_workflow.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_publish_images_workflow.py
RENAMED
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_python_version_alignment.py
RENAMED
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_release_version_check.py
RENAMED
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_render_example_profile.py
RENAMED
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_resolve_profile_path.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_smoke_example_profile.py
RENAMED
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_superset_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{composable_data_stack-0.5.0 → composable_data_stack-0.5.1}/tests/test_upstream_image_hardening.py
RENAMED
|
File without changes
|
|
File without changes
|