atlas-init 0.8.0__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 CHANGED
@@ -1,6 +1,6 @@
1
1
  from pathlib import Path
2
2
 
3
- VERSION = "0.8.0"
3
+ VERSION = "0.9.0"
4
4
 
5
5
 
6
6
  def running_in_repo() -> bool:
@@ -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(settings, base_url)
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(module_path: Path) -> list[Path]:
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 (module_path / "examples").glob("*")
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 ModuleGenConfig
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
- def read_examples(module: ModuleGenConfig) -> str:
47
- example_dirs = read_example_dirs(module.module_out_path)
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(module: ModuleGenConfig) -> str:
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", module.readme_path().name),
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 generate_readme(module: ModuleGenConfig) -> str:
95
- readme_path = module.readme_path()
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
- update_between_markers(
100
- readme_path,
101
- _readme_disclaimer,
102
- ReadmeMarkers.as_start(ReadmeMarkers.DISCLAIMER),
103
- ReadmeMarkers.as_end(ReadmeMarkers.DISCLAIMER),
104
- )
105
- run_and_wait("terraform fmt -recursive .", cwd=module.module_out_path, allow_non_zero_exit=True, ansi_content=False)
106
- example_section = read_examples(module)
107
- update_between_markers(
108
- readme_path,
109
- example_section,
110
- ReadmeMarkers.as_start(ReadmeMarkers.EXAMPLE),
111
- ReadmeMarkers.as_end(ReadmeMarkers.EXAMPLE),
112
- )
113
- docs_config_path = module.terraform_docs_config_path()
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(module)
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=module.module_out_path)
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
@@ -128,9 +128,11 @@ def generate_resource_variables(
128
128
  resource: type[ResourceAbs] | None, resource_config: ResourceGenConfig, extra_skipped: set[str] | None = None
129
129
  ) -> str:
130
130
  extra_skipped = extra_skipped or set()
131
- required_variables = resource_config.required_variables
132
131
  if resource is None:
133
132
  return ""
133
+ required_variables = set(resource_config.required_variables)
134
+ if not resource_config.use_opt_in_required_variables:
135
+ required_variables |= getattr(resource, ResourceAbs.REQUIRED_ATTRIBUTES_NAME, set())
134
136
  out = []
135
137
  hints = get_type_hints(resource)
136
138
  ignored_names = (
@@ -145,7 +147,8 @@ def generate_resource_variables(
145
147
  return format_tf_content(f'''variable "{resource_config.name}" {{
146
148
  type = {tf_type}
147
149
  }}\n''')
148
- for f in fields(resource): # type: ignore
150
+ fields_sorted = sorted(fields(resource), key=lambda f: (0 if f.name in required_variables else 1, f.name))
151
+ for f in fields_sorted: # type: ignore
149
152
  field_name = f.name
150
153
  if field_name.isupper() or field_name in ignored_names:
151
154
  continue
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
- from typing import Self
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
@@ -74,6 +77,7 @@ def as_import_line(name: str) -> str:
74
77
  class ResourceGenConfig(Entity):
75
78
  name: str
76
79
  use_single_variable: bool = False
80
+ use_opt_in_required_variables: bool = False
77
81
  required_variables: set[str] = PydanticField(default_factory=set)
78
82
  skip_variables_extra: set[str] = PydanticField(default_factory=set)
79
83
  attribute_default_hcl_strings: dict[str, str] = PydanticField(default_factory=dict)
@@ -252,12 +256,13 @@ class ModuleGenConfig(Entity):
252
256
  return resource_type
253
257
  raise ValueError(f"Could not resolve resource type for path {path}")
254
258
 
259
+ @property
255
260
  def readme_path(self) -> Path:
256
- return self.module_out_path / "README.md"
261
+ return self.module_out_path / README_FILENAME
257
262
 
258
263
  @property
259
264
  def examples_path(self) -> Path:
260
- return self.module_out_path / "examples"
265
+ return self.module_out_path / EXAMPLES_DIRNAME
261
266
 
262
267
  def example_name(self, name: str, example_nr: int) -> str:
263
268
  return f"{example_nr:02d}_{name}"
@@ -266,7 +271,7 @@ class ModuleGenConfig(Entity):
266
271
  return self.examples_path / name
267
272
 
268
273
  def terraform_docs_config_path(self) -> Path:
269
- return self.module_out_path / ".terraform-docs.yml"
274
+ return self.module_out_path / TERRAFORM_DOCS_CONFIG_FILENAME
270
275
 
271
276
 
272
277
  @dataclass
@@ -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 typing import Iterable, NamedTuple
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 = parse_graphs(example_dirs, task)
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).strip('"').strip()
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(example_dirs: list[Path], task: new_task, max_workers: int = 16, max_dirs: int = 9999) -> AtlasGraph:
239
- atlas_graph = AtlasGraph()
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
- graphs = {}
247
- for future in futures:
296
+ for future, example_dir in futures.items():
248
297
  try:
249
- example_dir, graph_output = future.result()
298
+ _, graph = future.result()
250
299
  except ShellError as e:
251
- logger.error(f"Error parsing graph for {futures[future]}: {e}")
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
- try:
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
- dots = pydot.graph_from_dot_data(graph_output) # not thread safe, so we use the main thread here instead
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, str]:
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
- return example_dir, graph_output
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
@@ -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.gen_module_readme import generate_readme
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
- if readme_path := config.readme_path():
196
- with new_task("Generating README.md"):
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.module_out_path)
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:
@@ -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(skipped_module_resource_types, settings, atlas_graph):
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
  )
@@ -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.8.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=HszEP86pa4V8Bem1Xhq0i_TtYub_SoFcTi_pWmwwq0I,213
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=5TMc_depk9MOkfEfq3WWWgyCpPmAvoZywmeNnU0QZGg,8520
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=mjYCZ7Sx-OTsjv7AZ4qyjXqvq3TOaczLfDjOQK_dKWk,2012
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=nN-KhPFs3PnW4PWgEop4fXFTQf1jP6A3pGdKFRWBmCA,4803
128
- atlas_init/tf_ext/gen_module_readme.py,sha256=04Lu9dYRMnYeKoQrIclpxUCT5YZctNzK71mzhFr_07E,4421
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
- atlas_init/tf_ext/gen_resource_variables.py,sha256=sykS9tpp1YZLXLx6kQanCwGj7XtHGoVpqGmPQYvsOB8,6690
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=8tJmLtwzzuNp_waG2eZ9bFmqRx6dJeYivu-ygvJUiLM,3631
134
- atlas_init/tf_ext/models_module.py,sha256=mgl8zX7EyszMjVdKb8g9IfoBK7BNeNXaT3zDcPGgJSs,18065
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=mHCfI0LpL1bCru9m2Rh7otGbCexh0lUiZoDAguB1A4g,12229
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/tf_mod_gen.py,sha256=wsn9gxu2IKpOEE_KS09e5e-3VT4xjs73cjL_HSWmIo4,12250
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=_JokgP-i1iS-iEXVI0MMNorrBHjFG6XcJRKiUe6JbvY,16200
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=gGVdttrSzUX2uzTAeXzvCF4BQnMxrkltAAc3_BCsiGc,968
150
- atlas_init-0.8.0.dist-info/METADATA,sha256=EcUiqi9up4csGk08KKIOV3qjrifcra3qYJynHoIq6Hs,5863
151
- atlas_init-0.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
152
- atlas_init-0.8.0.dist-info/entry_points.txt,sha256=l38KdfCjY2v5q8Ves1qkWNvPTPND6Tp2EKX-RL-MN3c,200
153
- atlas_init-0.8.0.dist-info/licenses/LICENSE,sha256=aKnucPyXnK1A-aXn4vac71zRpcB5BXjDyl4PDyi_hZg,1069
154
- atlas_init-0.8.0.dist-info/RECORD,,
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,,