atlas-init 0.8.1__py3-none-any.whl → 0.9.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.
- atlas_init/__init__.py +1 -1
- atlas_init/cli_root/trigger.py +1 -1
- atlas_init/settings/rich_utils.py +22 -0
- atlas_init/tf_ext/gen_examples.py +4 -2
- atlas_init/tf_ext/{gen_module_readme.py → gen_readme.py} +46 -26
- atlas_init/tf_ext/models.py +27 -1
- atlas_init/tf_ext/models_module.py +7 -3
- atlas_init/tf_ext/tf_dep.py +69 -22
- atlas_init/tf_ext/tf_example_readme.py +392 -0
- atlas_init/tf_ext/tf_mod_gen.py +4 -6
- atlas_init/tf_ext/tf_modules.py +3 -1
- atlas_init/tf_ext/typer_app.py +2 -1
- {atlas_init-0.8.1.dist-info → atlas_init-0.9.0.dist-info}/METADATA +1 -1
- {atlas_init-0.8.1.dist-info → atlas_init-0.9.0.dist-info}/RECORD +17 -16
- {atlas_init-0.8.1.dist-info → atlas_init-0.9.0.dist-info}/WHEEL +0 -0
- {atlas_init-0.8.1.dist-info → atlas_init-0.9.0.dist-info}/entry_points.txt +0 -0
- {atlas_init-0.8.1.dist-info → atlas_init-0.9.0.dist-info}/licenses/LICENSE +0 -0
atlas_init/__init__.py
CHANGED
atlas_init/cli_root/trigger.py
CHANGED
@@ -30,7 +30,7 @@ def create_realm_app():
|
|
30
30
|
project_id = atlas_settings.MONGODB_ATLAS_PROJECT_ID
|
31
31
|
base_url = atlas_settings.realm_url
|
32
32
|
cluster_name = cluster_settings.MONGODB_ATLAS_CLUSTER_NAME
|
33
|
-
auth_headers = login_to_realm(
|
33
|
+
auth_headers = login_to_realm(atlas_settings, base_url)
|
34
34
|
realm_settings = env_vars_cls_or_none(RealmSettings, dotenv_path=settings.env_vars_trigger)
|
35
35
|
if realm_settings and function_exists(
|
36
36
|
base_url,
|
@@ -1,6 +1,8 @@
|
|
1
1
|
import logging
|
2
2
|
from typing import Literal
|
3
3
|
|
4
|
+
from rich.console import Console
|
5
|
+
from rich.tree import Tree
|
4
6
|
import typer
|
5
7
|
from pydantic import BaseModel
|
6
8
|
from rich.logging import RichHandler
|
@@ -62,3 +64,23 @@ def configure_logging(
|
|
62
64
|
app.pretty_exceptions_show_locals = False
|
63
65
|
|
64
66
|
return handler
|
67
|
+
|
68
|
+
|
69
|
+
# https://github.com/Textualize/rich/blob/8c4d3d1d50047e3aaa4140d0ffc1e0c9f1df5af4/tests/test_live.py#L11
|
70
|
+
def create_capture_console(*, width: int = 60, height: int = 80, force_terminal: bool = True) -> Console:
|
71
|
+
return Console(
|
72
|
+
width=width,
|
73
|
+
height=height,
|
74
|
+
force_terminal=force_terminal,
|
75
|
+
legacy_windows=False,
|
76
|
+
color_system=None, # use no color system to reduce complexity of output,
|
77
|
+
_environ={},
|
78
|
+
)
|
79
|
+
|
80
|
+
|
81
|
+
def tree_text(tree: Tree) -> str:
|
82
|
+
console = create_capture_console()
|
83
|
+
console.width = 10_000
|
84
|
+
console.begin_capture()
|
85
|
+
console.print(tree)
|
86
|
+
return "\n".join(line.rstrip() for line in console.end_capture().splitlines())
|
@@ -19,10 +19,12 @@ def _examples_casted(examples: dict) -> dict[str, ResourceAbs]:
|
|
19
19
|
return examples
|
20
20
|
|
21
21
|
|
22
|
-
def read_example_dirs(
|
22
|
+
def read_example_dirs(examples_dir: Path) -> list[Path]:
|
23
|
+
if not examples_dir.exists():
|
24
|
+
return []
|
23
25
|
return sorted(
|
24
26
|
example_dir
|
25
|
-
for example_dir in
|
27
|
+
for example_dir in examples_dir.glob("*")
|
26
28
|
if example_dir.is_dir()
|
27
29
|
and len(example_dir.name) > 2
|
28
30
|
and example_dir.name[:2].isdigit()
|
@@ -1,11 +1,14 @@
|
|
1
|
+
from __future__ import annotations
|
1
2
|
import logging
|
2
3
|
from enum import StrEnum
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Callable, TypeAlias
|
3
6
|
|
4
7
|
from ask_shell import run_and_wait
|
5
8
|
from zero_3rdparty.file_utils import ensure_parents_write_text, update_between_markers
|
6
9
|
|
7
10
|
from atlas_init.tf_ext.gen_examples import read_example_dirs
|
8
|
-
from atlas_init.tf_ext.models_module import
|
11
|
+
from atlas_init.tf_ext.models_module import EXAMPLES_DIRNAME, README_FILENAME, TERRAFORM_DOCS_CONFIG_FILENAME
|
9
12
|
|
10
13
|
logger = logging.getLogger(__name__)
|
11
14
|
_readme_disclaimer = """\
|
@@ -13,12 +16,13 @@ _readme_disclaimer = """\
|
|
13
16
|
This Module is not meant for external consumption.
|
14
17
|
It is part of a development PoC.
|
15
18
|
Any usage problems will not be supported.
|
16
|
-
However, if you have any ideas or feedback feel free to open a Github Issue!
|
19
|
+
However, if you have any ideas or feedback, feel free to open a Github Issue!
|
17
20
|
"""
|
18
21
|
|
19
22
|
|
20
23
|
class ReadmeMarkers(StrEnum):
|
21
24
|
DISCLAIMER = "DISCLAIMER"
|
25
|
+
MODULES = "MODULES"
|
22
26
|
EXAMPLE = "TF_EXAMPLES"
|
23
27
|
TF_DOCS = "TF_DOCS"
|
24
28
|
|
@@ -42,9 +46,23 @@ class ReadmeMarkers(StrEnum):
|
|
42
46
|
def example_boilerplate(cls) -> str:
|
43
47
|
return "\n".join(cls.marker_lines(marker_name) for marker_name in list(cls))
|
44
48
|
|
49
|
+
@classmethod
|
50
|
+
def readme_generators(cls) -> ReadmeGenerators:
|
51
|
+
return [
|
52
|
+
(cls.DISCLAIMER, lambda _: _readme_disclaimer),
|
53
|
+
(cls.EXAMPLE, lambda workspace: read_examples(workspace / EXAMPLES_DIRNAME)),
|
54
|
+
]
|
55
|
+
|
56
|
+
|
57
|
+
ReadmeGenerators: TypeAlias = list[tuple[ReadmeMarkers, Callable[[Path], str]]]
|
45
58
|
|
46
|
-
|
47
|
-
|
59
|
+
|
60
|
+
def read_examples(examples_dir: Path) -> str:
|
61
|
+
example_dirs = read_example_dirs(examples_dir)
|
62
|
+
if not example_dirs:
|
63
|
+
return ""
|
64
|
+
# ensure the examples are formatted first
|
65
|
+
run_and_wait("terraform fmt -recursive .", cwd=examples_dir.parent, allow_non_zero_exit=True, ansi_content=False)
|
48
66
|
content = ["# Examples"]
|
49
67
|
for example_dir in example_dirs:
|
50
68
|
example_name = example_dir.name
|
@@ -80,10 +98,10 @@ sort:
|
|
80
98
|
"""
|
81
99
|
|
82
100
|
|
83
|
-
def terraform_docs_config_content(
|
101
|
+
def terraform_docs_config_content(readme_path: Path) -> str:
|
84
102
|
config = _static_terraform_config
|
85
103
|
for replacement_in, replacement_out in [
|
86
|
-
("FILENAME",
|
104
|
+
("FILENAME", readme_path.name),
|
87
105
|
("START_MARKER", ReadmeMarkers.as_start(ReadmeMarkers.TF_DOCS)),
|
88
106
|
("END_MARKER", ReadmeMarkers.as_end(ReadmeMarkers.TF_DOCS)),
|
89
107
|
]:
|
@@ -91,36 +109,38 @@ def terraform_docs_config_content(module: ModuleGenConfig) -> str:
|
|
91
109
|
return config
|
92
110
|
|
93
111
|
|
94
|
-
def
|
95
|
-
|
112
|
+
def generate_and_write_readme(terraform_workdir: Path, *, generators: ReadmeGenerators | None = None) -> str:
|
113
|
+
generators = generators or ReadmeMarkers.readme_generators()
|
114
|
+
readme_path = terraform_workdir / README_FILENAME
|
96
115
|
assert readme_path.exists(), (
|
97
116
|
f"{readme_path} does not exist, currently a boilerplate is expected, consider adding to {readme_path}\n{ReadmeMarkers.example_boilerplate()}"
|
98
117
|
)
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
118
|
+
for marker, generator in generators:
|
119
|
+
content = generator(terraform_workdir)
|
120
|
+
if not content:
|
121
|
+
continue
|
122
|
+
update_between_markers(
|
123
|
+
readme_path,
|
124
|
+
content,
|
125
|
+
ReadmeMarkers.as_start(marker),
|
126
|
+
ReadmeMarkers.as_end(marker),
|
127
|
+
)
|
128
|
+
generate_terraform_docs(readme_path)
|
129
|
+
logger.info(f"updated {readme_path}")
|
130
|
+
return readme_path.read_text()
|
131
|
+
|
132
|
+
|
133
|
+
def generate_terraform_docs(readme_path: Path) -> None:
|
134
|
+
docs_config_path = readme_path.parent / TERRAFORM_DOCS_CONFIG_FILENAME
|
114
135
|
if docs_config_path.exists():
|
115
136
|
logger.warning(f"{docs_config_path} already exists, skipping generation")
|
116
137
|
else:
|
117
|
-
config_content = terraform_docs_config_content(
|
138
|
+
config_content = terraform_docs_config_content(readme_path)
|
118
139
|
ensure_parents_write_text(docs_config_path, config_content)
|
119
140
|
logger.info(f"generated {docs_config_path}")
|
120
|
-
run_and_wait(f"terraform-docs -c {docs_config_path} .", cwd=
|
141
|
+
run_and_wait(f"terraform-docs -c {docs_config_path} .", cwd=readme_path.parent)
|
121
142
|
readme_content = _default_link_updater(readme_path.read_text())
|
122
143
|
ensure_parents_write_text(readme_path, readme_content)
|
123
|
-
return readme_path.read_text()
|
124
144
|
|
125
145
|
|
126
146
|
def _default_link_updater(readme_content: str) -> str: # can be a global replacer for now
|
atlas_init/tf_ext/models.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
from
|
2
|
+
from dataclasses import dataclass, field
|
3
|
+
from typing import Iterable, Self
|
3
4
|
|
4
5
|
from model_lib import Entity
|
5
6
|
from pydantic import Field, RootModel, model_validator
|
@@ -30,6 +31,31 @@ def choose_next_emoji() -> str:
|
|
30
31
|
return emoji
|
31
32
|
|
32
33
|
|
34
|
+
@dataclass
|
35
|
+
class EmojiCounter:
|
36
|
+
counter: int = 0
|
37
|
+
existing_emojis: dict[str, str] = field(default_factory=dict)
|
38
|
+
|
39
|
+
@property
|
40
|
+
def emoji_to_names(self) -> dict[str, str]:
|
41
|
+
return dict(zip(self.existing_emojis.values(), self.existing_emojis.keys()))
|
42
|
+
|
43
|
+
def emoji_name(self) -> Iterable[tuple[str, str]]:
|
44
|
+
src = self.emoji_to_names
|
45
|
+
for emoji in _emojii_list:
|
46
|
+
if emoji in src:
|
47
|
+
yield emoji, src[emoji]
|
48
|
+
else:
|
49
|
+
break
|
50
|
+
|
51
|
+
def get_emoji(self, name: str) -> str:
|
52
|
+
if existing := self.existing_emojis.get(name):
|
53
|
+
return existing
|
54
|
+
emoji = self.existing_emojis[name] = _emojii_list[self.counter]
|
55
|
+
self.counter += 1
|
56
|
+
return emoji
|
57
|
+
|
58
|
+
|
33
59
|
class ModuleState(Entity):
|
34
60
|
resource_types: set[str] = Field(default_factory=set, description="Set of resource types in the module.")
|
35
61
|
|
@@ -24,6 +24,9 @@ from atlas_init.tf_ext.py_gen import (
|
|
24
24
|
from atlas_init.tf_ext.settings import RepoOut, TfExtSettings
|
25
25
|
|
26
26
|
ResourceTypeT: TypeAlias = str
|
27
|
+
TERRAFORM_DOCS_CONFIG_FILENAME: str = ".terraform-docs.yml"
|
28
|
+
README_FILENAME: str = "README.md"
|
29
|
+
EXAMPLES_DIRNAME: str = "examples"
|
27
30
|
|
28
31
|
|
29
32
|
@dataclass
|
@@ -253,12 +256,13 @@ class ModuleGenConfig(Entity):
|
|
253
256
|
return resource_type
|
254
257
|
raise ValueError(f"Could not resolve resource type for path {path}")
|
255
258
|
|
259
|
+
@property
|
256
260
|
def readme_path(self) -> Path:
|
257
|
-
return self.module_out_path /
|
261
|
+
return self.module_out_path / README_FILENAME
|
258
262
|
|
259
263
|
@property
|
260
264
|
def examples_path(self) -> Path:
|
261
|
-
return self.module_out_path /
|
265
|
+
return self.module_out_path / EXAMPLES_DIRNAME
|
262
266
|
|
263
267
|
def example_name(self, name: str, example_nr: int) -> str:
|
264
268
|
return f"{example_nr:02d}_{name}"
|
@@ -267,7 +271,7 @@ class ModuleGenConfig(Entity):
|
|
267
271
|
return self.examples_path / name
|
268
272
|
|
269
273
|
def terraform_docs_config_path(self) -> Path:
|
270
|
-
return self.module_out_path /
|
274
|
+
return self.module_out_path / TERRAFORM_DOCS_CONFIG_FILENAME
|
271
275
|
|
272
276
|
|
273
277
|
@dataclass
|
atlas_init/tf_ext/tf_dep.py
CHANGED
@@ -1,16 +1,18 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
|
+
from functools import total_ordering
|
3
4
|
import logging
|
4
5
|
from collections import defaultdict
|
5
6
|
from pathlib import Path
|
6
|
-
from
|
7
|
+
from threading import RLock
|
8
|
+
from typing import Callable, Iterable, NamedTuple
|
7
9
|
|
8
10
|
import pydot
|
9
11
|
from ask_shell import ShellError, new_task, run_and_wait
|
10
12
|
from ask_shell._run import stop_runs_and_pool
|
11
13
|
from ask_shell.run_pool import run_pool
|
12
14
|
from model_lib import Entity, dump
|
13
|
-
from pydantic import BaseModel, Field
|
15
|
+
from pydantic import BaseModel, Field, model_validator
|
14
16
|
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
|
15
17
|
from typer import Typer
|
16
18
|
from zero_3rdparty.file_utils import ensure_parents_write_text
|
@@ -57,13 +59,25 @@ def tf_dep_graph(
|
|
57
59
|
example_dirs = get_example_directories(repo_path, skip_names)
|
58
60
|
logger.info(f"example_dirs: \n{'\n'.join(str(d) for d in sorted(example_dirs))}")
|
59
61
|
with new_task("Find terraform graphs", total=len(example_dirs)) as task:
|
60
|
-
atlas_graph =
|
62
|
+
atlas_graph = create_atlas_graph(example_dirs, task)
|
61
63
|
with new_task("Dump graph"):
|
62
64
|
graph_yaml = atlas_graph.dump_yaml()
|
63
65
|
ensure_parents_write_text(settings.atlas_graph_path, graph_yaml)
|
64
66
|
logger.info(f"Atlas graph dumped to {settings.atlas_graph_path}")
|
65
67
|
|
66
68
|
|
69
|
+
def create_atlas_graph(example_dirs: list[Path], task: new_task) -> AtlasGraph:
|
70
|
+
atlas_graph = AtlasGraph()
|
71
|
+
|
72
|
+
def on_graph(example_dir: Path, graph: pydot.Dot):
|
73
|
+
atlas_graph.add_edges(graph.get_edges())
|
74
|
+
atlas_graph.add_variable_edges(example_dir)
|
75
|
+
|
76
|
+
parse_graphs(on_graph, example_dirs, task)
|
77
|
+
|
78
|
+
return atlas_graph
|
79
|
+
|
80
|
+
|
67
81
|
def print_edges(graph: pydot.Dot):
|
68
82
|
edges = graph.get_edges()
|
69
83
|
for edge in edges:
|
@@ -79,9 +93,15 @@ class ResourceParts(NamedTuple):
|
|
79
93
|
return self.resource_type.split("_")[0]
|
80
94
|
|
81
95
|
|
96
|
+
@total_ordering
|
82
97
|
class ResourceRef(BaseModel):
|
83
98
|
full_ref: str
|
84
99
|
|
100
|
+
@model_validator(mode="after")
|
101
|
+
def ensure_plain(self):
|
102
|
+
self.full_ref = plain_name(self.full_ref)
|
103
|
+
return self
|
104
|
+
|
85
105
|
def _resource_parts(self) -> ResourceParts:
|
86
106
|
match self.full_ref.split("."):
|
87
107
|
case [resource_type, resource_name] if "_" in resource_type:
|
@@ -106,6 +126,11 @@ class ResourceRef(BaseModel):
|
|
106
126
|
def is_module(self) -> bool:
|
107
127
|
return self.full_ref.startswith(MODULE_PREFIX)
|
108
128
|
|
129
|
+
@property
|
130
|
+
def module_name(self) -> str:
|
131
|
+
assert self.is_module, f"ResourceRef {self.full_ref} is not a module"
|
132
|
+
return self.full_ref.removeprefix(MODULE_PREFIX).split(".")[0]
|
133
|
+
|
109
134
|
@property
|
110
135
|
def is_data(self) -> bool:
|
111
136
|
return self.full_ref.startswith(DATA_PREFIX)
|
@@ -114,6 +139,22 @@ class ResourceRef(BaseModel):
|
|
114
139
|
def resource_type(self) -> str:
|
115
140
|
return self._resource_parts().resource_type
|
116
141
|
|
142
|
+
def __lt__(self, other: object) -> bool:
|
143
|
+
if not isinstance(other, ResourceRef):
|
144
|
+
raise TypeError(f"cannot compare {type(self)} with {type(other)}")
|
145
|
+
return self.full_ref < other.full_ref
|
146
|
+
|
147
|
+
def __hash__(self) -> int:
|
148
|
+
return hash(self.full_ref)
|
149
|
+
|
150
|
+
def __eq__(self, other: object) -> bool:
|
151
|
+
if not isinstance(other, ResourceRef):
|
152
|
+
return NotImplemented
|
153
|
+
return self.full_ref == other.full_ref
|
154
|
+
|
155
|
+
def __str__(self) -> str:
|
156
|
+
return self.full_ref
|
157
|
+
|
117
158
|
|
118
159
|
class EdgeParsed(BaseModel):
|
119
160
|
parent: ResourceRef
|
@@ -149,7 +190,15 @@ class EdgeParsed(BaseModel):
|
|
149
190
|
|
150
191
|
|
151
192
|
def edge_plain(edge_endpoint: pydot.EdgeEndpoint) -> str:
|
152
|
-
return str(edge_endpoint)
|
193
|
+
return plain_name(str(edge_endpoint))
|
194
|
+
|
195
|
+
|
196
|
+
def node_plain(node: pydot.Node) -> str:
|
197
|
+
return plain_name(node.get_name())
|
198
|
+
|
199
|
+
|
200
|
+
def plain_name(name: str) -> str:
|
201
|
+
return name.strip('"').strip()
|
153
202
|
|
154
203
|
|
155
204
|
def edge_src_dest(edge: pydot.Edge) -> tuple[str, str]:
|
@@ -235,34 +284,27 @@ class AtlasGraph(Entity):
|
|
235
284
|
self.parent_child_edges[parent_type].add(child_type)
|
236
285
|
|
237
286
|
|
238
|
-
def parse_graphs(
|
239
|
-
|
287
|
+
def parse_graphs(
|
288
|
+
on_graph: Callable[[Path, pydot.Dot], None], example_dirs: list[Path], task: new_task, max_dirs: int = 1_000
|
289
|
+
) -> None:
|
240
290
|
with run_pool("parse example graphs", total=len(example_dirs)) as executor:
|
241
291
|
futures = {
|
242
292
|
executor.submit(parse_graph, example_dir): example_dir
|
243
293
|
for i, example_dir in enumerate(example_dirs)
|
244
294
|
if i < max_dirs
|
245
295
|
}
|
246
|
-
|
247
|
-
for future in futures:
|
296
|
+
for future, example_dir in futures.items():
|
248
297
|
try:
|
249
|
-
|
298
|
+
_, graph = future.result()
|
250
299
|
except ShellError as e:
|
251
|
-
logger.error(f"Error parsing graph for {
|
300
|
+
logger.error(f"Error parsing graph for {example_dir}: {e}")
|
252
301
|
continue
|
253
302
|
except KeyboardInterrupt:
|
254
303
|
logger.error("KeyboardInterrupt received, stopping graph parsing.")
|
255
304
|
stop_runs_and_pool("KeyboardInterrupt", immediate=True)
|
256
305
|
break
|
257
|
-
|
258
|
-
graph = graphs[example_dir] = parse_graph_output(example_dir, graph_output)
|
259
|
-
except GraphParseError as e:
|
260
|
-
logger.error(e)
|
261
|
-
continue
|
262
|
-
atlas_graph.add_edges(graph.get_edges())
|
263
|
-
atlas_graph.add_variable_edges(example_dir)
|
306
|
+
on_graph(example_dir, graph)
|
264
307
|
task.update(advance=1)
|
265
|
-
return atlas_graph
|
266
308
|
|
267
309
|
|
268
310
|
class GraphParseError(Exception):
|
@@ -271,9 +313,13 @@ class GraphParseError(Exception):
|
|
271
313
|
super().__init__(f"Failed to parse graph for {example_dir}: {message}")
|
272
314
|
|
273
315
|
|
316
|
+
_lock = RLock()
|
317
|
+
|
318
|
+
|
274
319
|
def parse_graph_output(example_dir: Path, graph_output: str, verbose: bool = False) -> pydot.Dot:
|
275
320
|
assert graph_output, f"Graph output is empty for {example_dir}"
|
276
|
-
|
321
|
+
with _lock:
|
322
|
+
dots = pydot.graph_from_dot_data(graph_output)
|
277
323
|
if not dots:
|
278
324
|
raise GraphParseError(example_dir, f"No graphs found in the output:\n{graph_output}")
|
279
325
|
assert len(dots) == 1, f"Expected one graph for {example_dir}, got {len(dots)}"
|
@@ -297,10 +343,10 @@ class EmptyGraphOutputError(Exception):
|
|
297
343
|
@retry(
|
298
344
|
stop=stop_after_attempt(3),
|
299
345
|
wait=wait_fixed(1),
|
300
|
-
retry=retry_if_exception_type(EmptyGraphOutputError),
|
346
|
+
retry=retry_if_exception_type((EmptyGraphOutputError, GraphParseError)),
|
301
347
|
reraise=True,
|
302
348
|
)
|
303
|
-
def parse_graph(example_dir: Path) -> tuple[Path,
|
349
|
+
def parse_graph(example_dir: Path) -> tuple[Path, pydot.Dot]:
|
304
350
|
env_vars = {
|
305
351
|
"MONGODB_ATLAS_PREVIEW_PROVIDER_V2_ADVANCED_CLUSTER": "true" if is_v2_example_dir(example_dir) else "false",
|
306
352
|
}
|
@@ -309,7 +355,8 @@ def parse_graph(example_dir: Path) -> tuple[Path, str]:
|
|
309
355
|
run_and_wait("terraform init", cwd=example_dir, env=env_vars)
|
310
356
|
run = run_and_wait("terraform graph", cwd=example_dir, env=env_vars)
|
311
357
|
if graph_output := run.stdout_one_line:
|
312
|
-
|
358
|
+
graph = parse_graph_output(example_dir, graph_output) # just to make sure we get no errors
|
359
|
+
return example_dir, graph
|
313
360
|
raise EmptyGraphOutputError(example_dir)
|
314
361
|
|
315
362
|
|
@@ -0,0 +1,392 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from collections import defaultdict
|
3
|
+
from contextlib import suppress
|
4
|
+
from functools import total_ordering
|
5
|
+
import logging
|
6
|
+
from pathlib import Path
|
7
|
+
from typing import Callable, ClassVar, Iterable, Protocol, TypeAlias
|
8
|
+
from ask_shell import new_task
|
9
|
+
from ask_shell.rich_live import get_live_console
|
10
|
+
from model_lib import Entity, parse_dict
|
11
|
+
from pydantic import Field, model_validator
|
12
|
+
import pydot
|
13
|
+
from rich.tree import Tree
|
14
|
+
import typer
|
15
|
+
|
16
|
+
from atlas_init.settings.rich_utils import tree_text
|
17
|
+
from atlas_init.tf_ext.gen_readme import ReadmeMarkers, generate_and_write_readme
|
18
|
+
from atlas_init.tf_ext.models import EmojiCounter
|
19
|
+
from atlas_init.tf_ext.models_module import README_FILENAME
|
20
|
+
from atlas_init.tf_ext.tf_dep import EdgeParsed, ResourceRef, node_plain, parse_graph, parse_graphs
|
21
|
+
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
MODULES_JSON_RELATIVE_PATH = ".terraform/modules/modules.json"
|
24
|
+
|
25
|
+
|
26
|
+
def tf_example_readme(
|
27
|
+
example_path: Path = typer.Option(
|
28
|
+
..., "-e", "--example-path", help="Path to the example directory", default_factory=Path.cwd
|
29
|
+
),
|
30
|
+
skip_module_details: list[str] = typer.Option(
|
31
|
+
..., "-s", "--skip-module-details", help="List of module details to skip", default_factory=list
|
32
|
+
),
|
33
|
+
):
|
34
|
+
with new_task("parse example graph"):
|
35
|
+
_, example_graph_dot = parse_graph(example_path) # ensures init is called
|
36
|
+
example_graph = ResourceGraph.from_graph(example_graph_dot)
|
37
|
+
with new_task("parse module graphs") as task:
|
38
|
+
modules_config = parse_modules_json(example_path, skip_module_details)
|
39
|
+
module_paths = modules_config.module_paths
|
40
|
+
module_graphs: dict[Path, ResourceGraph] = {}
|
41
|
+
|
42
|
+
def on_graph(example_dir: Path, graph: pydot.Dot):
|
43
|
+
module_graphs[example_dir] = ResourceGraph.from_graph(graph)
|
44
|
+
|
45
|
+
parse_graphs(on_graph, module_paths, task)
|
46
|
+
with new_task("create example module graph"):
|
47
|
+
# a graph when all resources in a module are treated as a single node.
|
48
|
+
modules_graph, emoji_counter = create_module_graph(example_graph)
|
49
|
+
with new_task(f"update {README_FILENAME}"):
|
50
|
+
modules_section = []
|
51
|
+
modules_trees_texts = []
|
52
|
+
module_dirs_used: set[Path] = set()
|
53
|
+
|
54
|
+
def add_module_tree(module_dir: Path):
|
55
|
+
# trees are only once per module, not per module instance
|
56
|
+
if module_dir in module_dirs_used:
|
57
|
+
return
|
58
|
+
module_dirs_used.add(module_dir)
|
59
|
+
module_graph = module_graphs[module_dir]
|
60
|
+
module_config = modules_config.get_by_path(module_dir)
|
61
|
+
emojis = ", ".join(emoji_counter.get_emoji(key) for key in module_config.keys)
|
62
|
+
if modules_config.skip_details(module_config):
|
63
|
+
tree = Tree(f"{module_dir.name} ({emojis})")
|
64
|
+
tree.add("details skipped")
|
65
|
+
else:
|
66
|
+
tree = module_graph.to_tree(f"{module_dir.name} ({emojis})", include_orphans=True)
|
67
|
+
get_live_console().print(tree)
|
68
|
+
modules_trees_texts.append(tree_text(tree))
|
69
|
+
|
70
|
+
for _, module_key in emoji_counter.emoji_name():
|
71
|
+
module_config = modules_config.get_by_key(module_key)
|
72
|
+
module_dir = module_config.absolute_path(example_path)
|
73
|
+
add_module_tree(module_dir)
|
74
|
+
|
75
|
+
def add_module_src(node: Tree, name: str) -> None:
|
76
|
+
config = modules_config.get_by_key(name)
|
77
|
+
node.add(f"{config.source}")
|
78
|
+
|
79
|
+
module_index_tree = emoji_tree(emoji_counter, tree_processor=add_module_src, name="Module Instances")
|
80
|
+
modules_section.extend(
|
81
|
+
[
|
82
|
+
"## Modules",
|
83
|
+
"",
|
84
|
+
"### Modules Instances",
|
85
|
+
"```sh",
|
86
|
+
tree_text(module_index_tree),
|
87
|
+
"```",
|
88
|
+
"### Module Definitions",
|
89
|
+
"",
|
90
|
+
"```sh",
|
91
|
+
"\n".join(modules_trees_texts),
|
92
|
+
"```",
|
93
|
+
"",
|
94
|
+
"### Graph with Dependencies",
|
95
|
+
"Any resource without a number prefix is defined at the root level.",
|
96
|
+
"",
|
97
|
+
as_mermaid(modules_graph),
|
98
|
+
]
|
99
|
+
)
|
100
|
+
generators = ReadmeMarkers.readme_generators()
|
101
|
+
generators.insert(1, (ReadmeMarkers.MODULES, lambda _: "\n".join(modules_section)))
|
102
|
+
generate_and_write_readme(
|
103
|
+
example_path,
|
104
|
+
generators=generators,
|
105
|
+
)
|
106
|
+
|
107
|
+
|
108
|
+
def create_module_graph(example_graph: ResourceGraph) -> tuple[ResourceGraph, EmojiCounter]:
|
109
|
+
emoji_counter = EmojiCounter()
|
110
|
+
|
111
|
+
def as_module_edge(parent: ResourceRef, child: ResourceRef) -> bool | ParentChild:
|
112
|
+
if not child.is_module:
|
113
|
+
return False
|
114
|
+
new_parent = add_emoji_prefix(parent, emoji_counter)
|
115
|
+
new_child = add_emoji_prefix(child, emoji_counter)
|
116
|
+
return new_parent, new_child
|
117
|
+
|
118
|
+
return create_subgraph(example_graph, as_module_edge), emoji_counter
|
119
|
+
|
120
|
+
|
121
|
+
def emoji_tree(
|
122
|
+
counter: EmojiCounter, *, tree_processor: Callable[[Tree, str], None] | None = None, name: str = "Emoji Tree"
|
123
|
+
) -> Tree:
|
124
|
+
tree = Tree(name)
|
125
|
+
for emoji, name in counter.emoji_name():
|
126
|
+
child = tree.add(f"{emoji} {name}")
|
127
|
+
if tree_processor:
|
128
|
+
tree_processor(child, name)
|
129
|
+
return tree
|
130
|
+
|
131
|
+
|
132
|
+
def add_emoji_prefix(ref: ResourceRef, emoji_counter: EmojiCounter) -> ResourceRef:
|
133
|
+
if ref.is_module:
|
134
|
+
return ResourceRef(full_ref=f"{emoji_counter.get_emoji(ref.module_name)} {ref.module_name}")
|
135
|
+
return ref
|
136
|
+
|
137
|
+
|
138
|
+
def strip_emoji_prefix(ref: ResourceRef) -> ResourceRef:
|
139
|
+
old_ref = ref.full_ref
|
140
|
+
return ResourceRef(full_ref=old_ref.split(" ")[1]) if " " in old_ref else ref
|
141
|
+
|
142
|
+
|
143
|
+
def as_module_name(ref: ResourceRef) -> str:
|
144
|
+
if ref.is_module:
|
145
|
+
return ref.module_name
|
146
|
+
return ""
|
147
|
+
|
148
|
+
|
149
|
+
def as_module_ref(ref: ResourceRef) -> ResourceRef:
|
150
|
+
if name := as_module_name(ref):
|
151
|
+
return ResourceRef(full_ref=f"module.{name}")
|
152
|
+
return ref
|
153
|
+
|
154
|
+
|
155
|
+
class _RootModuleIgnored(Exception):
|
156
|
+
pass
|
157
|
+
|
158
|
+
|
159
|
+
ParentChild: TypeAlias = tuple[ResourceRef, ResourceRef]
|
160
|
+
|
161
|
+
|
162
|
+
class ResourceGraph(Entity):
|
163
|
+
IGNORED_ORPHANS: ClassVar[set[str]] = {"node"} # some extra output from `terraform graph` command
|
164
|
+
parent_children: dict[ResourceRef, set[ResourceRef]] = Field(default_factory=lambda: defaultdict(set))
|
165
|
+
children_parents: dict[ResourceRef, set[ResourceRef]] = Field(default_factory=lambda: defaultdict(set))
|
166
|
+
orphans: set[ResourceRef] = Field(default_factory=set)
|
167
|
+
|
168
|
+
@classmethod
|
169
|
+
def from_graph(cls, graph: pydot.Dot) -> "ResourceGraph":
|
170
|
+
resource_graph = cls()
|
171
|
+
resource_graph.add_edges(graph.get_edges())
|
172
|
+
for orphan in graph.get_node_list():
|
173
|
+
name = node_plain(orphan)
|
174
|
+
if name in cls.IGNORED_ORPHANS:
|
175
|
+
continue
|
176
|
+
ref = ResourceRef(full_ref=name)
|
177
|
+
resource_graph.add_orphan_if_not_found(ref)
|
178
|
+
return resource_graph
|
179
|
+
|
180
|
+
def add_orphan_if_not_found(self, orphan: ResourceRef):
|
181
|
+
if orphan not in self.parent_children and orphan not in self.children_parents:
|
182
|
+
self.orphans.add(orphan)
|
183
|
+
|
184
|
+
def add_edges(self, edges: list[pydot.Edge]):
|
185
|
+
for edge in edges:
|
186
|
+
parsed = EdgeParsed.from_edge(edge)
|
187
|
+
parent = parsed.parent
|
188
|
+
child = parsed.child
|
189
|
+
if str(parent) == ".this":
|
190
|
+
logger.info(f"parent: {parent} child: {child}")
|
191
|
+
self.add_edge(parent, child)
|
192
|
+
|
193
|
+
def add_edge(self, parent: ResourceRef, child: ResourceRef):
|
194
|
+
self.parent_children[parent].add(child)
|
195
|
+
self.children_parents[child].add(parent)
|
196
|
+
|
197
|
+
def all_edges(self) -> list[ParentChild]:
|
198
|
+
return [(parent, child) for parent in self.parent_children for child in self.parent_children[parent]]
|
199
|
+
|
200
|
+
@property
|
201
|
+
def all_parents(self) -> set[ResourceRef]:
|
202
|
+
return set(self.parent_children.keys())
|
203
|
+
|
204
|
+
def sorted_parents(self) -> Iterable[ResourceRef]:
|
205
|
+
used_parents = set()
|
206
|
+
remaining_parents = self.all_parents
|
207
|
+
|
208
|
+
def next_parent() -> ResourceRef | None:
|
209
|
+
candidates = [parent for parent in remaining_parents if not self.children_parents[parent] - used_parents]
|
210
|
+
return min(candidates) if candidates else None
|
211
|
+
|
212
|
+
while remaining_parents:
|
213
|
+
parent = next_parent()
|
214
|
+
if parent is None:
|
215
|
+
break
|
216
|
+
used_parents.add(parent)
|
217
|
+
yield parent
|
218
|
+
remaining_parents.remove(parent)
|
219
|
+
|
220
|
+
def to_tree(self, example_dir_name: str, include_orphans: bool = False) -> Tree:
|
221
|
+
root = Tree(example_dir_name)
|
222
|
+
trees: dict[ResourceRef, Tree] = {}
|
223
|
+
|
224
|
+
for parent in self.sorted_parents():
|
225
|
+
parent_tree = trees.setdefault(parent, Tree(parent.full_ref))
|
226
|
+
for child_ref in sorted(self.parent_children[parent]):
|
227
|
+
child_tree = trees.setdefault(child_ref, Tree(child_ref.full_ref))
|
228
|
+
parent_tree.add(child_tree)
|
229
|
+
if not self.children_parents[parent]:
|
230
|
+
root.add(parent_tree)
|
231
|
+
if include_orphans:
|
232
|
+
for orphan in sorted(self.orphans):
|
233
|
+
if orphan not in trees:
|
234
|
+
root.add(Tree(orphan.full_ref))
|
235
|
+
return root
|
236
|
+
|
237
|
+
|
238
|
+
def as_mermaid(graph: ResourceGraph) -> str:
|
239
|
+
nodes: dict[str, str] = {}
|
240
|
+
|
241
|
+
def mermaid_ref(ref: ResourceRef) -> str:
|
242
|
+
no_emoji = strip_emoji_prefix(ref)
|
243
|
+
mermaid = str(no_emoji)
|
244
|
+
assert '"' not in mermaid, f"mermaid ref should not contain quotes: {mermaid}"
|
245
|
+
nodes[mermaid] = str(ref) # want original ref in label
|
246
|
+
return mermaid
|
247
|
+
|
248
|
+
def add_mermaid_edge(parent: ResourceRef, child: ResourceRef) -> str:
|
249
|
+
parent_ref = mermaid_ref(parent)
|
250
|
+
child_ref = mermaid_ref(child)
|
251
|
+
return f"{parent_ref} --> {child_ref}"
|
252
|
+
|
253
|
+
edges = [add_mermaid_edge(parent, child) for parent, child in graph.all_edges()]
|
254
|
+
return "\n".join(
|
255
|
+
[
|
256
|
+
"```mermaid",
|
257
|
+
"graph TD",
|
258
|
+
" " + "\n ".join(f'{mermaid}["{label}"]' for mermaid, label in sorted(nodes.items())),
|
259
|
+
" " + "\n ".join(sorted(edges)),
|
260
|
+
"```",
|
261
|
+
]
|
262
|
+
)
|
263
|
+
|
264
|
+
|
265
|
+
class EdgeFilter(Protocol):
|
266
|
+
def __call__(self, parent: ResourceRef, child: ResourceRef) -> bool | ParentChild: ...
|
267
|
+
|
268
|
+
|
269
|
+
def create_subgraph(graph: ResourceGraph, edge_filter: EdgeFilter) -> ResourceGraph:
|
270
|
+
subgraph = ResourceGraph()
|
271
|
+
for parent in graph.sorted_parents():
|
272
|
+
for child in sorted(graph.parent_children[parent]):
|
273
|
+
filter_response = edge_filter(parent, child)
|
274
|
+
match filter_response:
|
275
|
+
case True:
|
276
|
+
subgraph.add_edge(parent, child)
|
277
|
+
case False:
|
278
|
+
continue
|
279
|
+
case (parent, child) if parent != child:
|
280
|
+
subgraph.add_edge(parent, child)
|
281
|
+
return subgraph
|
282
|
+
|
283
|
+
|
284
|
+
@total_ordering
|
285
|
+
class ModuleExampleConfig(Entity):
|
286
|
+
keys: list[str]
|
287
|
+
rel_path: str = Field(alias="Dir", description="Relative path to the module example")
|
288
|
+
source: str = Field(
|
289
|
+
alias="Source",
|
290
|
+
description="Source of the module, for example: registry.terraform.io/terraform-aws-modules/vpc/aws",
|
291
|
+
)
|
292
|
+
version: str = Field(
|
293
|
+
alias="Version", description="Version of the module example, unset for local modules", default=""
|
294
|
+
)
|
295
|
+
|
296
|
+
@model_validator(mode="before")
|
297
|
+
@classmethod
|
298
|
+
def move_key(cls, v: dict):
|
299
|
+
key = v.pop("Key", None)
|
300
|
+
if key:
|
301
|
+
v["keys"] = [key]
|
302
|
+
if v.get("Dir", "") == ".":
|
303
|
+
raise _RootModuleIgnored()
|
304
|
+
return v
|
305
|
+
|
306
|
+
@model_validator(mode="after")
|
307
|
+
def validate_keys(self):
|
308
|
+
if not self.keys:
|
309
|
+
raise ValueError("keys is required")
|
310
|
+
return self
|
311
|
+
|
312
|
+
@property
|
313
|
+
def key(self) -> str:
|
314
|
+
return ",".join(sorted(self.keys))
|
315
|
+
|
316
|
+
def absolute_path(self, example_path: Path) -> Path:
|
317
|
+
path = example_path / self.rel_path
|
318
|
+
if not path.exists():
|
319
|
+
raise ValueError(f"module path not found for {self.key}: {path}")
|
320
|
+
return path
|
321
|
+
|
322
|
+
def __lt__(self, other: object) -> bool:
|
323
|
+
if not isinstance(other, ModuleExampleConfig):
|
324
|
+
raise TypeError(f"cannot compare {type(self)} with {type(other)}")
|
325
|
+
return self.key < other.key
|
326
|
+
|
327
|
+
|
328
|
+
class ModuleExampleConfigs(Entity):
|
329
|
+
example_path: Path
|
330
|
+
modules: dict[str, ModuleExampleConfig] = Field(default_factory=dict)
|
331
|
+
skip_module_details: set[str] = Field(default_factory=set)
|
332
|
+
|
333
|
+
@model_validator(mode="after")
|
334
|
+
def ensure_paths_exists(self):
|
335
|
+
not_exists: dict[str, Path] = {}
|
336
|
+
for config in self.modules.values():
|
337
|
+
path = config.absolute_path(self.example_path)
|
338
|
+
if not path.exists():
|
339
|
+
not_exists[config.key] = path
|
340
|
+
if not_exists:
|
341
|
+
raise ValueError(f"module paths not found: {not_exists}")
|
342
|
+
return self
|
343
|
+
|
344
|
+
@property
|
345
|
+
def module_paths(self) -> list[Path]:
|
346
|
+
return [config.absolute_path(self.example_path) for config in self.modules.values()]
|
347
|
+
|
348
|
+
def skip_details(self, config: ModuleExampleConfig) -> bool:
|
349
|
+
return any(key in self.skip_module_details for key in config.keys)
|
350
|
+
|
351
|
+
def get_by_path(self, module_dir: Path) -> ModuleExampleConfig:
|
352
|
+
for config in self.modules.values():
|
353
|
+
if config.absolute_path(self.example_path) == module_dir:
|
354
|
+
return config
|
355
|
+
raise ValueError(f"module not found for {module_dir}")
|
356
|
+
|
357
|
+
def get_by_key_or_none(self, key: str) -> ModuleExampleConfig | None:
|
358
|
+
return self.modules.get(key)
|
359
|
+
|
360
|
+
def get_by_key(self, key: str) -> ModuleExampleConfig:
|
361
|
+
return self.modules[key]
|
362
|
+
|
363
|
+
def modules_included(self, *, skip_keys: list[str]) -> list[ModuleExampleConfig]:
|
364
|
+
return [config for config in self.modules.values() if all(key not in skip_keys for key in config.keys)]
|
365
|
+
|
366
|
+
def add_module(self, config: ModuleExampleConfig):
|
367
|
+
key = config.keys[0]
|
368
|
+
assert len(config.keys) == 1, "only one key can be added at a time"
|
369
|
+
source = config.source
|
370
|
+
existing_config = next(
|
371
|
+
(existing_config for existing_config in self.modules.values() if source == existing_config.source),
|
372
|
+
None,
|
373
|
+
)
|
374
|
+
if existing_config:
|
375
|
+
existing_config.keys.append(key)
|
376
|
+
existing_config.keys.sort()
|
377
|
+
self.modules[key] = existing_config
|
378
|
+
else:
|
379
|
+
self.modules[key] = config
|
380
|
+
|
381
|
+
|
382
|
+
def parse_modules_json(example_path: Path, skip_module_details: list[str] | None = None) -> ModuleExampleConfigs:
|
383
|
+
configs = ModuleExampleConfigs(example_path=example_path, skip_module_details=set(skip_module_details or []))
|
384
|
+
module_json_path = example_path / MODULES_JSON_RELATIVE_PATH
|
385
|
+
if not module_json_path.exists():
|
386
|
+
return configs
|
387
|
+
module_json = parse_dict(module_json_path)
|
388
|
+
for raw in module_json.get("Modules", []):
|
389
|
+
with suppress(_RootModuleIgnored):
|
390
|
+
config = ModuleExampleConfig(**raw)
|
391
|
+
configs.add_module(config)
|
392
|
+
return configs
|
atlas_init/tf_ext/tf_mod_gen.py
CHANGED
@@ -12,7 +12,7 @@ from zero_3rdparty.file_utils import clean_dir, copy, ensure_parents_write_text
|
|
12
12
|
from atlas_init.cli_tf.example_update import UpdateExamples, update_examples
|
13
13
|
from atlas_init.tf_ext.args import TF_CLI_CONFIG_FILE_ARG
|
14
14
|
from atlas_init.tf_ext.gen_examples import generate_module_examples, read_example_dirs
|
15
|
-
from atlas_init.tf_ext.
|
15
|
+
from atlas_init.tf_ext.gen_readme import generate_and_write_readme
|
16
16
|
from atlas_init.tf_ext.gen_resource_main import generate_resource_main
|
17
17
|
from atlas_init.tf_ext.gen_resource_output import generate_resource_output
|
18
18
|
from atlas_init.tf_ext.gen_resource_variables import generate_module_variables
|
@@ -192,12 +192,10 @@ def module_examples_and_readme(config: ModuleGenConfig, *, example_var_file: Pat
|
|
192
192
|
if out_event.changes:
|
193
193
|
logger.info(f"Updated attribute descriptions: {len(out_event.changes)}")
|
194
194
|
run_and_wait("terraform fmt -recursive .", cwd=path, ansi_content=False, allow_non_zero_exit=True)
|
195
|
-
|
196
|
-
|
197
|
-
readme_content = generate_readme(config)
|
198
|
-
ensure_parents_write_text(readme_path, readme_content)
|
195
|
+
with new_task("Generating README.md"):
|
196
|
+
generate_and_write_readme(config.module_out_path)
|
199
197
|
if example_var_file:
|
200
|
-
examples = read_example_dirs(config.
|
198
|
+
examples = read_example_dirs(config.examples_path)
|
201
199
|
if examples:
|
202
200
|
failed_examples: list[Path] = []
|
203
201
|
with run_pool("Running terraform plan on examples", total=len(examples), exit_wait_timeout=60) as pool:
|
atlas_init/tf_ext/tf_modules.py
CHANGED
@@ -175,7 +175,9 @@ def tf_modules(
|
|
175
175
|
logger.info(f"Missing modules: \n{'\n'.join(missing_resources)}")
|
176
176
|
|
177
177
|
|
178
|
-
def generate_module_graphs(
|
178
|
+
def generate_module_graphs(
|
179
|
+
skipped_module_resource_types: Iterable[str], settings: TfExtSettings, atlas_graph: AtlasGraph
|
180
|
+
):
|
179
181
|
tree = Tree(
|
180
182
|
"Module graphs",
|
181
183
|
)
|
atlas_init/tf_ext/typer_app.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
from ask_shell import configure_logging
|
2
2
|
from typer import Typer
|
3
3
|
|
4
|
-
from atlas_init.tf_ext import api_call, settings, tf_desc_gen, tf_mod_gen_provider
|
4
|
+
from atlas_init.tf_ext import api_call, settings, tf_desc_gen, tf_example_readme, tf_mod_gen_provider
|
5
5
|
|
6
6
|
|
7
7
|
def typer_main():
|
@@ -20,6 +20,7 @@ def typer_main():
|
|
20
20
|
app.command(name="api-config")(api_call.api_config)
|
21
21
|
app.command(name="mod-gen-provider")(tf_mod_gen_provider.tf_mod_gen_provider_resource_modules)
|
22
22
|
app.command(name="check-env-vars")(settings.init_tf_ext_settings)
|
23
|
+
app.command(name="example-readme")(tf_example_readme.tf_example_readme)
|
23
24
|
configure_logging(app)
|
24
25
|
app()
|
25
26
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: atlas-init
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.9.0
|
4
4
|
Project-URL: Documentation, https://github.com/EspenAlbert/atlas-init#readme
|
5
5
|
Project-URL: Issues, https://github.com/EspenAlbert/atlas-init/issues
|
6
6
|
Project-URL: Source, https://github.com/EspenAlbert/atlas-init
|
@@ -1,4 +1,4 @@
|
|
1
|
-
atlas_init/__init__.py,sha256=
|
1
|
+
atlas_init/__init__.py,sha256=SxwCA6YCJ-sDX3qOsnEH3L4WjI9tXH14OB1liYWsnHw,213
|
2
2
|
atlas_init/__main__.py,sha256=dY1dWWvwxRZMmnOFla6RSfti-hMeLeKdoXP7SVYqMUc,52
|
3
3
|
atlas_init/atlas_init.yaml,sha256=aCCg6PJgLcqh75v3tdEEG4UPrcJlbmbRaLj0c7PCumE,2373
|
4
4
|
atlas_init/cli.py,sha256=0b4_osi67Oj-hZnrtK70IbSKQDREjwUFv1_30_A03Zg,9447
|
@@ -23,7 +23,7 @@ atlas_init/cli_helper/tf_runner.py,sha256=V8pfxPSDBSeQWkR27kI8JLu-MEadUR9TGnoXBj
|
|
23
23
|
atlas_init/cli_root/__init__.py,sha256=Mf0wqy4kqq8pmbjLa98zOGuUWv0bLk2OYGc1n1_ZmZ4,223
|
24
24
|
atlas_init/cli_root/go_test.py,sha256=roQIOS-qVfNhJMztR-V3hjtxFMf7-Ioy3e1ffqtTRyo,4601
|
25
25
|
atlas_init/cli_root/mms_released.py,sha256=gaUWzY4gqb1Tuo7u-8HqkOC6pW8QSjcwTz2UJMCV3Cw,1765
|
26
|
-
atlas_init/cli_root/trigger.py,sha256=
|
26
|
+
atlas_init/cli_root/trigger.py,sha256=vT32qeWq946r8UaU8ZUKtWafpU0kodbUJpQXctFGjjk,8526
|
27
27
|
atlas_init/cli_tf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
28
28
|
atlas_init/cli_tf/app.py,sha256=SDIWy1B8cEIA5ieqL8fEhVHwQdK919MjdCSiWgB1M8M,8522
|
29
29
|
atlas_init/cli_tf/changelog.py,sha256=biWYKf1pZvXZ-jEgcZ5q9sY7nTGrL2PuI0h9mCILf_g,3181
|
@@ -82,7 +82,7 @@ atlas_init/settings/env_vars_modules.py,sha256=fqVndOn8ZesuudijakzPZKsrXnm-iezGH
|
|
82
82
|
atlas_init/settings/interactive.py,sha256=Xy1Z5WMAOSaJ-vQI_4xjAbSR92rWQgnffwVoDT27L68,340
|
83
83
|
atlas_init/settings/interactive2.py,sha256=Z0-AptV19Eq1GizouOUhD-vA8IWnTejo_urnS7D4gdY,3856
|
84
84
|
atlas_init/settings/path.py,sha256=vFRmoUHQplWuKsICu8uHPNAYhsuJu_xoVhmTnii_p7M,2301
|
85
|
-
atlas_init/settings/rich_utils.py,sha256=
|
85
|
+
atlas_init/settings/rich_utils.py,sha256=MSk-sqj-5B0dMBL4Dvq-B1nursXbxOxKZzqEBRogcQU,2765
|
86
86
|
atlas_init/tf/.terraform.lock.hcl,sha256=8petzLKtZy0Yi-Iy5QmkdkkuwRfHnPJSskQhlHROpyo,6880
|
87
87
|
atlas_init/tf/always.tf,sha256=WbatrKzdmeV_fWNjO8u3dw96o5nvfB1ciZ4H4E7aSvM,1398
|
88
88
|
atlas_init/tf/main.tf,sha256=O-Q72u8iJ9bS8tQPx7VR5dwG925QcyuUCxY92VR7Vng,4623
|
@@ -124,14 +124,14 @@ atlas_init/tf_ext/__main__.py,sha256=pCZ1j_TIo1lFCBdfrqw-EGta2dEs_oONd_IktW3jlOA
|
|
124
124
|
atlas_init/tf_ext/api_call.py,sha256=vJmCkP-fMLM4qLcFxzbTZUKSxk5Yx1U3tHQZAKFyJ0I,12868
|
125
125
|
atlas_init/tf_ext/args.py,sha256=m77eU6DPPD4vFpMWPR7UqH65vBp3AGV7fyXDIe-uORE,1031
|
126
126
|
atlas_init/tf_ext/constants.py,sha256=FU8JA-JvbkbtpRykilMCKk7YiREPFpZI0hW_My4kvJE,202
|
127
|
-
atlas_init/tf_ext/gen_examples.py,sha256=
|
128
|
-
atlas_init/tf_ext/
|
127
|
+
atlas_init/tf_ext/gen_examples.py,sha256=b_1d7rdLofqmH8d-Q6hjpbhcUXRrkIQYqvj51CeVZLI,4842
|
128
|
+
atlas_init/tf_ext/gen_readme.py,sha256=9CjQRJnMHeLJMGfF9k9DvcApk9I64vaB5cKwVPA6gNs,5153
|
129
129
|
atlas_init/tf_ext/gen_resource_main.py,sha256=C-aqkOFXH5ffT51igPtNW7szIH-MkY3HnMcSZ0Q4OdE,7263
|
130
130
|
atlas_init/tf_ext/gen_resource_output.py,sha256=_LM3mKMVmcqm2uWokvIfMxOnyCD4aCDUNGUp5DujF7E,3518
|
131
131
|
atlas_init/tf_ext/gen_resource_variables.py,sha256=hgaHJlgnk1bNCV46TlvUIVZ5PVUQvxxnfo-hKN05SEU,6954
|
132
132
|
atlas_init/tf_ext/gen_versions.py,sha256=yYX4jIgQfAZuG2wfu6OsIbDAqt9g8btiLcgKm7DvNqM,469
|
133
|
-
atlas_init/tf_ext/models.py,sha256=
|
134
|
-
atlas_init/tf_ext/models_module.py,sha256=
|
133
|
+
atlas_init/tf_ext/models.py,sha256=vS725jiEc0t61IO1Db0mpxWVQPGaJKIGeZLOb2SOf4I,4430
|
134
|
+
atlas_init/tf_ext/models_module.py,sha256=JngaAxN-54hYPffx1rZ-_j-hYT_GELq7lf24e-EQHb0,18276
|
135
135
|
atlas_init/tf_ext/newres.py,sha256=quMSLlkJRuvA3attTvJ-DQNSwRPFyT_XJ32ucmEhA-s,3104
|
136
136
|
atlas_init/tf_ext/paths.py,sha256=3VQri_VKS5sVKdkHu2XKgHziB1uQpB8hBGBY7GwiAwU,4917
|
137
137
|
atlas_init/tf_ext/plan_diffs.py,sha256=Sc-VFrq2k7p01ZZzgtAfqRXCog2h6bbFlonGfaxSzog,5237
|
@@ -139,16 +139,17 @@ atlas_init/tf_ext/provider_schema.py,sha256=6XrJ3UHjpQ_8yBqWeZ1b4xIPLOqPL9L1ljn4
|
|
139
139
|
atlas_init/tf_ext/py_gen.py,sha256=orZLX2c-tw9eJb8MyTunhkdfqs-f6uOngu5RlLUb9uI,11243
|
140
140
|
atlas_init/tf_ext/schema_to_dataclass.py,sha256=kqg0OxqYZwmNGvlayZkXPwU2CueiHsnAAgLyoXkDUtY,19797
|
141
141
|
atlas_init/tf_ext/settings.py,sha256=qRSm8e_TquEKoBpYE6VFmXSpiRaHExR00cVykpK1_OI,6814
|
142
|
-
atlas_init/tf_ext/tf_dep.py,sha256=
|
142
|
+
atlas_init/tf_ext/tf_dep.py,sha256=UFrNmolw3qW02E7mq23vVGY0C8HOSSY3-6AHmSEqYno,13453
|
143
143
|
atlas_init/tf_ext/tf_desc_gen.py,sha256=G6Hv9kKJDNbNL8nRRkPA7w8c7MCDgijjdTv-2BjKrZ0,2575
|
144
144
|
atlas_init/tf_ext/tf_desc_update.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
145
|
-
atlas_init/tf_ext/
|
145
|
+
atlas_init/tf_ext/tf_example_readme.py,sha256=A8yBJq9Ts0NzBnurWbbmADrfursjhxfdWX3ZxkYZ5cs,14992
|
146
|
+
atlas_init/tf_ext/tf_mod_gen.py,sha256=mOXFfKoghkeV-CCYbqknwII5LmY1iguoUsTjYxndwi4,12141
|
146
147
|
atlas_init/tf_ext/tf_mod_gen_provider.py,sha256=M7jJeH0VR1uN440d2H5-9lnj0s0d9QtaMeYsLwWW3Ak,5866
|
147
|
-
atlas_init/tf_ext/tf_modules.py,sha256=
|
148
|
+
atlas_init/tf_ext/tf_modules.py,sha256=h_E8Po1fmOcIjU_d92dCjKZytBij4N9Mux70yLW7xVQ,16248
|
148
149
|
atlas_init/tf_ext/tf_vars.py,sha256=A__zcIBNvJ5y0l5F5G7KMQsYCuyr3iBZtRzXm4e6DQU,7875
|
149
|
-
atlas_init/tf_ext/typer_app.py,sha256=
|
150
|
-
atlas_init-0.
|
151
|
-
atlas_init-0.
|
152
|
-
atlas_init-0.
|
153
|
-
atlas_init-0.
|
154
|
-
atlas_init-0.
|
150
|
+
atlas_init/tf_ext/typer_app.py,sha256=5Zqgs5RMYitlGmPyyXq3WctRcWMRC6RQa7EWJhVeQlA,1063
|
151
|
+
atlas_init-0.9.0.dist-info/METADATA,sha256=uOYn0ME4hd1OvjjpfksJWWCC_aV0XNqklFTFbjzygh0,5863
|
152
|
+
atlas_init-0.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
153
|
+
atlas_init-0.9.0.dist-info/entry_points.txt,sha256=l38KdfCjY2v5q8Ves1qkWNvPTPND6Tp2EKX-RL-MN3c,200
|
154
|
+
atlas_init-0.9.0.dist-info/licenses/LICENSE,sha256=aKnucPyXnK1A-aXn4vac71zRpcB5BXjDyl4PDyi_hZg,1069
|
155
|
+
atlas_init-0.9.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|