lely 0.1.0__py3-none-any.whl
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.
- lely/__init__.py +15 -0
- lely/__main__.py +6 -0
- lely/approval.py +206 -0
- lely/cli.py +1315 -0
- lely/config.py +582 -0
- lely/databricks.py +64 -0
- lely/errors.py +26 -0
- lely/github.py +507 -0
- lely/model.py +463 -0
- lely/options.py +355 -0
- lely/planfile.py +721 -0
- lely/planning.py +663 -0
- lely/process.py +56 -0
- lely/py.typed +0 -0
- lely/refs.py +370 -0
- lely/registry.py +198 -0
- lely/render/__init__.py +2 -0
- lely/render/html.py +542 -0
- lely/render/markdown.py +586 -0
- lely/render/rich.py +323 -0
- lely/render/words.py +285 -0
- lely/running.py +420 -0
- lely/schema.py +275 -0
- lely/source.py +312 -0
- lely/step.py +244 -0
- lely/steps/__init__.py +1 -0
- lely/steps/bundle.py +550 -0
- lely/steps/bundle_run.py +99 -0
- lely/steps/command.py +293 -0
- lely/steps/stevin.py +152 -0
- lely/testing.py +238 -0
- lely-0.1.0.dist-info/METADATA +291 -0
- lely-0.1.0.dist-info/RECORD +36 -0
- lely-0.1.0.dist-info/WHEEL +4 -0
- lely-0.1.0.dist-info/entry_points.txt +8 -0
- lely-0.1.0.dist-info/licenses/LICENSE +202 -0
lely/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""One plan for your whole Databricks deploy.
|
|
2
|
+
|
|
3
|
+
The bundle and everything around it — the steps before, the steps after —
|
|
4
|
+
reviewed before anything runs, and taken down again when you say so. `spec/`
|
|
5
|
+
says what each piece must deliver; `docs/DESIGN.md` says how it is built.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
__version__ = version("lely")
|
|
14
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
15
|
+
__version__ = "0.0.0"
|
lely/__main__.py
ADDED
lely/approval.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""What may run: the approval check, and the checks on a plan file.
|
|
2
|
+
|
|
3
|
+
Pure. Two sentences carry it: a plan reaches as far as lely can see, and
|
|
4
|
+
consent covers what was shown.
|
|
5
|
+
|
|
6
|
+
Every refusal here is a `Refused`: running the same thing again won't help, it
|
|
7
|
+
takes a new plan.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
from lely.errors import Refused
|
|
15
|
+
from lely.model import (
|
|
16
|
+
Change,
|
|
17
|
+
Input,
|
|
18
|
+
Plan,
|
|
19
|
+
PlannedStep,
|
|
20
|
+
Secret,
|
|
21
|
+
Source,
|
|
22
|
+
StepPlan,
|
|
23
|
+
Value,
|
|
24
|
+
Workspace,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
AGAIN = "Plan again."
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def same_project(approved: Plan, steps: Sequence[tuple[str, str, str]]) -> None:
|
|
31
|
+
"""Refuse a plan file whose steps, or any step's options as written, differ
|
|
32
|
+
from the project's now. `steps` are (name, uses, made-from hash), in order.
|
|
33
|
+
|
|
34
|
+
What is compared is the config as lely reads it, not the file's text — an
|
|
35
|
+
unrelated edit doesn't make a plan stale — with environment values by name.
|
|
36
|
+
"""
|
|
37
|
+
planned = [step.name for step in approved.steps]
|
|
38
|
+
current = [name for name, _, _ in steps]
|
|
39
|
+
if planned != current:
|
|
40
|
+
raise Refused(
|
|
41
|
+
f"The plan was made for the steps {', '.join(planned) or 'none'}; the "
|
|
42
|
+
f"project now has {', '.join(current) or 'none'}. {AGAIN}"
|
|
43
|
+
)
|
|
44
|
+
for step, (name, uses, made_from) in zip(approved.steps, steps, strict=True):
|
|
45
|
+
if step.uses != uses or step.made_from != made_from:
|
|
46
|
+
raise Refused(
|
|
47
|
+
f"Step `{name}` is configured differently than when the plan was "
|
|
48
|
+
f"made. {AGAIN}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def same_workspace(approved: Plan, workspace: Workspace) -> None:
|
|
53
|
+
"""A plan made against one workspace is refused on another. Consent is
|
|
54
|
+
given to a target *on a workspace*, not to a name that could mean anything.
|
|
55
|
+
|
|
56
|
+
The identity may differ: a plan is made with credentials that can only
|
|
57
|
+
read, and applied with ones that can write.
|
|
58
|
+
"""
|
|
59
|
+
if _host(approved.workspace.host) != _host(workspace.host):
|
|
60
|
+
raise Refused(
|
|
61
|
+
f"The plan was made for {approved.workspace.host}, and this run talks to "
|
|
62
|
+
f"{workspace.host}. {AGAIN}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def same_source(approved: Plan, source: Source) -> None:
|
|
67
|
+
"""What was reviewed is what is deployed. A plan file is for a clean
|
|
68
|
+
checkout: refuse it for another project of the repository, on another
|
|
69
|
+
tree, and whenever something differs from `HEAD` — now, or when the plan
|
|
70
|
+
was made, since lely then can't say what it was made on.
|
|
71
|
+
|
|
72
|
+
A plan made outside a git repository, or in one with no commit yet,
|
|
73
|
+
recorded nothing it can be held to — it said so when it was made.
|
|
74
|
+
"""
|
|
75
|
+
planned = approved.source
|
|
76
|
+
if planned.tree is None:
|
|
77
|
+
return
|
|
78
|
+
if source.tree is None:
|
|
79
|
+
raise Refused(
|
|
80
|
+
"The plan was made on a commit of a git repository, and there is none "
|
|
81
|
+
f"here: lely can't check that the project is as it was planned. {AGAIN}"
|
|
82
|
+
)
|
|
83
|
+
if planned.root != source.root:
|
|
84
|
+
raise Refused(
|
|
85
|
+
f"The plan was made for the project in {_folder(planned.root)}, and this "
|
|
86
|
+
f"is the one in {_folder(source.root)}. {AGAIN}"
|
|
87
|
+
)
|
|
88
|
+
if planned.dirty:
|
|
89
|
+
raise Refused(
|
|
90
|
+
"The plan was made with uncommitted changes, so lely can't say what it "
|
|
91
|
+
"was made on. Plan again on a clean checkout — or run it without a file."
|
|
92
|
+
)
|
|
93
|
+
if source.dirty:
|
|
94
|
+
raise Refused(
|
|
95
|
+
f"The project has uncommitted changes that the plan was made without. {AGAIN}"
|
|
96
|
+
)
|
|
97
|
+
if source.tree != planned.tree:
|
|
98
|
+
raise Refused(
|
|
99
|
+
f"The plan was made on git tree {planned.tree[:12]}, and the project is "
|
|
100
|
+
f"now on {source.tree[:12]}. {AGAIN}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _folder(root: str | None) -> str:
|
|
105
|
+
return "the top of the repository" if root in (None, ".") else f"`{root}`"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def same_inputs(approved: PlannedStep, inputs: Sequence[Input]) -> None:
|
|
109
|
+
"""Every value a step took when it was planned must be the value it takes
|
|
110
|
+
now: the plan showed `model_version = 14`, and 15 was never approved.
|
|
111
|
+
|
|
112
|
+
The same value means the same kind of value: `1` is not `true`, `14` is
|
|
113
|
+
not `14.0` and not `"14"`. A secret can't be compared, and stays one: a
|
|
114
|
+
value the plan showed is not replaced by one nobody can see, nor the other
|
|
115
|
+
way round.
|
|
116
|
+
"""
|
|
117
|
+
known = {(one.label, one.source): one.value for one in approved.inputs if one.known}
|
|
118
|
+
for taken in inputs:
|
|
119
|
+
key = (taken.label, taken.source)
|
|
120
|
+
if key not in known or not taken.known:
|
|
121
|
+
continue
|
|
122
|
+
if not _unchanged(known[key], taken.value):
|
|
123
|
+
raise Refused(
|
|
124
|
+
f"Step `{approved.name}` takes {taken.source} = {_said(taken.value)} "
|
|
125
|
+
f"now; the plan was approved with {_said(known[key])}. {AGAIN}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _unchanged(was: Value, now: Value) -> bool:
|
|
130
|
+
if isinstance(was, Secret) or isinstance(now, Secret):
|
|
131
|
+
return isinstance(was, Secret) and isinstance(now, Secret)
|
|
132
|
+
return _same(was, now)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _said(value: Value) -> str:
|
|
136
|
+
return "a secret" if isinstance(value, Secret) else repr(value)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _same(one: object, other: object) -> bool:
|
|
140
|
+
"""Equal, and of one kind. Python calls `1 == True` and `14 == 14.0` true;
|
|
141
|
+
a reviewer who was shown one was not shown the other. A tuple and a list
|
|
142
|
+
of the same things are the same: one is what a plugin gave, the other what
|
|
143
|
+
a plan file read back."""
|
|
144
|
+
if isinstance(one, list | tuple) and isinstance(other, list | tuple):
|
|
145
|
+
return len(one) == len(other) and all(
|
|
146
|
+
_same(a, b) for a, b in zip(one, other, strict=True)
|
|
147
|
+
)
|
|
148
|
+
if isinstance(one, dict) and isinstance(other, dict):
|
|
149
|
+
return one.keys() == other.keys() and all(
|
|
150
|
+
_same(value, other[key]) for key, value in one.items()
|
|
151
|
+
)
|
|
152
|
+
return type(one) is type(other) and one == other
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def approved_changes(approved: StepPlan, fresh: StepPlan, step: str) -> None:
|
|
156
|
+
"""The approval check: every change in the fresh plan must be one that was
|
|
157
|
+
shown — the same thing, the same kind of change, the same lines.
|
|
158
|
+
|
|
159
|
+
Fewer changes is fine: someone else did part of the work, or an earlier run
|
|
160
|
+
did. Anything new, or anything that reads differently, stops the run.
|
|
161
|
+
"""
|
|
162
|
+
shown = {change.key: change for change in approved.changes}
|
|
163
|
+
for change in fresh.changes:
|
|
164
|
+
was = shown.get(change.key)
|
|
165
|
+
if was is None:
|
|
166
|
+
raise Refused(
|
|
167
|
+
f"Step `{step}` would now also {_says(change)}, which the approved "
|
|
168
|
+
f"plan didn't show. {AGAIN}"
|
|
169
|
+
)
|
|
170
|
+
if was != change:
|
|
171
|
+
raise Refused(
|
|
172
|
+
f"Step `{step}` would now {_says(change)}; the approved plan showed "
|
|
173
|
+
f"it would {_says(was)}. {AGAIN}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def destructive_allowed(steps: Sequence[PlannedStep], allow: bool) -> None:
|
|
178
|
+
"""A destructive change needs `--allow-destructive`. Where the plan already
|
|
179
|
+
shows one, this refuses before anything runs."""
|
|
180
|
+
if allow:
|
|
181
|
+
return
|
|
182
|
+
found = [
|
|
183
|
+
f"{step.name}: {change.summary}"
|
|
184
|
+
for step in steps
|
|
185
|
+
if step.state != "skipped"
|
|
186
|
+
for change in step.plan.changes
|
|
187
|
+
if change.destructive
|
|
188
|
+
]
|
|
189
|
+
if found:
|
|
190
|
+
raise Refused(
|
|
191
|
+
"The plan holds destructive changes — " + "; ".join(found) + ". "
|
|
192
|
+
"Pass --allow-destructive to apply them."
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _says(change: Change) -> str:
|
|
197
|
+
words = f"{change.action} {change.summary}"
|
|
198
|
+
if change.destructive and change.action not in ("delete", "replace"):
|
|
199
|
+
words += " (destructive)"
|
|
200
|
+
if change.detail:
|
|
201
|
+
words += f" [{'; '.join(change.detail)}]"
|
|
202
|
+
return words
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _host(host: str) -> str:
|
|
206
|
+
return host.rstrip("/").lower()
|