atlas-init 0.6.0__py3-none-any.whl → 0.8.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.
Files changed (65) hide show
  1. atlas_init/__init__.py +1 -1
  2. atlas_init/atlas_init.yaml +1 -0
  3. atlas_init/cli_args.py +19 -1
  4. atlas_init/cli_tf/ci_tests.py +116 -24
  5. atlas_init/cli_tf/example_update.py +20 -8
  6. atlas_init/cli_tf/go_test_run.py +14 -2
  7. atlas_init/cli_tf/go_test_summary.py +334 -82
  8. atlas_init/cli_tf/go_test_tf_error.py +20 -12
  9. atlas_init/cli_tf/hcl/modifier.py +22 -8
  10. atlas_init/cli_tf/hcl/modifier2.py +120 -0
  11. atlas_init/cli_tf/openapi.py +10 -6
  12. atlas_init/html_out/__init__.py +0 -0
  13. atlas_init/html_out/md_export.py +143 -0
  14. atlas_init/sdk_ext/__init__.py +0 -0
  15. atlas_init/sdk_ext/go.py +102 -0
  16. atlas_init/sdk_ext/typer_app.py +18 -0
  17. atlas_init/settings/env_vars.py +25 -3
  18. atlas_init/settings/env_vars_generated.py +2 -0
  19. atlas_init/tf/.terraform.lock.hcl +33 -33
  20. atlas_init/tf/modules/aws_s3/provider.tf +1 -1
  21. atlas_init/tf/modules/aws_vpc/provider.tf +1 -1
  22. atlas_init/tf/modules/cloud_provider/provider.tf +1 -1
  23. atlas_init/tf/modules/cluster/provider.tf +1 -1
  24. atlas_init/tf/modules/encryption_at_rest/provider.tf +1 -1
  25. atlas_init/tf/modules/federated_vars/federated_vars.tf +1 -2
  26. atlas_init/tf/modules/federated_vars/provider.tf +1 -1
  27. atlas_init/tf/modules/project_extra/provider.tf +1 -1
  28. atlas_init/tf/modules/stream_instance/provider.tf +1 -1
  29. atlas_init/tf/modules/vpc_peering/provider.tf +1 -1
  30. atlas_init/tf/modules/vpc_privatelink/versions.tf +1 -1
  31. atlas_init/tf/providers.tf +1 -1
  32. atlas_init/tf_ext/__init__.py +0 -0
  33. atlas_init/tf_ext/__main__.py +3 -0
  34. atlas_init/tf_ext/api_call.py +325 -0
  35. atlas_init/tf_ext/args.py +32 -0
  36. atlas_init/tf_ext/constants.py +3 -0
  37. atlas_init/tf_ext/gen_examples.py +141 -0
  38. atlas_init/tf_ext/gen_module_readme.py +131 -0
  39. atlas_init/tf_ext/gen_resource_main.py +195 -0
  40. atlas_init/tf_ext/gen_resource_output.py +71 -0
  41. atlas_init/tf_ext/gen_resource_variables.py +159 -0
  42. atlas_init/tf_ext/gen_versions.py +10 -0
  43. atlas_init/tf_ext/models.py +106 -0
  44. atlas_init/tf_ext/models_module.py +454 -0
  45. atlas_init/tf_ext/newres.py +90 -0
  46. atlas_init/tf_ext/paths.py +126 -0
  47. atlas_init/tf_ext/plan_diffs.py +140 -0
  48. atlas_init/tf_ext/provider_schema.py +199 -0
  49. atlas_init/tf_ext/py_gen.py +294 -0
  50. atlas_init/tf_ext/schema_to_dataclass.py +522 -0
  51. atlas_init/tf_ext/settings.py +188 -0
  52. atlas_init/tf_ext/tf_dep.py +324 -0
  53. atlas_init/tf_ext/tf_desc_gen.py +53 -0
  54. atlas_init/tf_ext/tf_desc_update.py +0 -0
  55. atlas_init/tf_ext/tf_mod_gen.py +263 -0
  56. atlas_init/tf_ext/tf_mod_gen_provider.py +124 -0
  57. atlas_init/tf_ext/tf_modules.py +395 -0
  58. atlas_init/tf_ext/tf_vars.py +158 -0
  59. atlas_init/tf_ext/typer_app.py +28 -0
  60. {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/METADATA +5 -3
  61. {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/RECORD +64 -31
  62. atlas_init-0.8.0.dist-info/entry_points.txt +5 -0
  63. atlas_init-0.6.0.dist-info/entry_points.txt +0 -2
  64. {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/WHEEL +0 -0
  65. {atlas_init-0.6.0.dist-info → atlas_init-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,159 @@
1
+ import logging
2
+ from contextlib import contextmanager
3
+ from dataclasses import Field, fields, is_dataclass
4
+ from typing import Dict, List, Set, Union, get_args, get_origin, get_type_hints
5
+
6
+ from model_lib import Entity
7
+ from pydantic import Field as PydanticField
8
+
9
+ from atlas_init.tf_ext.gen_resource_main import format_tf_content
10
+ from atlas_init.tf_ext.models_module import ResourceAbs, ResourceGenConfig, ResourceTypePythonModule
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class DefaultValueContext(Entity):
16
+ default_lines: list[str] = PydanticField(default_factory=list)
17
+ field_path: list[str] = PydanticField(default_factory=list)
18
+ ignored_names: set[str] = PydanticField(default_factory=set)
19
+
20
+ @property
21
+ def final_str(self) -> str:
22
+ if not self.default_lines:
23
+ return "null"
24
+ return "\n".join(self.default_lines)
25
+
26
+ @property
27
+ def current_field_path(self) -> str:
28
+ return ".".join(self.field_path)
29
+
30
+ def at_root(self, field_name: str) -> bool:
31
+ return field_name == self.current_field_path
32
+
33
+ @property
34
+ def _prefix(self) -> str:
35
+ return " " * len(self.field_path)
36
+
37
+ @contextmanager
38
+ def add_nested_field(self, field_name: str):
39
+ if not self.at_root(field_name):
40
+ self.field_path.append(field_name)
41
+ default_index_before = len(self.default_lines)
42
+ try:
43
+ yield
44
+ default_index_after = len(self.default_lines)
45
+ if (
46
+ default_index_before == default_index_after
47
+ ): # no child had a default, so we don't need to add default lines
48
+ return
49
+ if self.at_root(field_name):
50
+ self.default_lines.insert(default_index_before, "{")
51
+ else:
52
+ self.default_lines.insert(default_index_before, f"{self._prefix}{field_name} = {{")
53
+ self.default_lines.append(f"{self._prefix}}}")
54
+ finally:
55
+ self.field_path.pop()
56
+
57
+ def add_default(self, field_name: str, default_value: str) -> None:
58
+ if self.at_root(field_name): # root field default value, no field name needed
59
+ self.default_lines.append(default_value)
60
+ else:
61
+ self.default_lines.append(f"{self._prefix}{field_name} = {default_value}")
62
+
63
+
64
+ def python_type_to_terraform_type(field: Field, py_type: type, context: DefaultValueContext) -> str:
65
+ # Unwrap Optional/Union
66
+ origin = get_origin(py_type)
67
+ args = get_args(py_type)
68
+ if origin is Union and type(None) in args:
69
+ # Optional[X] or Union[X, None] -> X
70
+ not_none = [a for a in args if a is not type(None)]
71
+ return python_type_to_terraform_type(field, not_none[0], context) if not_none else "any"
72
+ if origin is list or origin is List:
73
+ elem_type = python_type_to_terraform_type(field, args[0], context)
74
+ return f"list({elem_type})"
75
+ elif origin is set or origin is Set:
76
+ elem_type = python_type_to_terraform_type(field, args[0], context)
77
+ return f"set({elem_type})"
78
+ elif origin is dict or origin is Dict:
79
+ elem_type = python_type_to_terraform_type(field, args[1], context)
80
+ return f"map({elem_type})"
81
+ elif is_dataclass(py_type):
82
+ return dataclass_to_object_type(field.name, py_type, context)
83
+ elif py_type is str:
84
+ return "string"
85
+ elif py_type is int or py_type is float:
86
+ return "number"
87
+ elif py_type is bool:
88
+ return "bool"
89
+ else:
90
+ return "any"
91
+
92
+
93
+ def dataclass_to_object_type(name: str, cls: type, context: DefaultValueContext) -> str:
94
+ lines = ["object({"]
95
+ hints = get_type_hints(cls)
96
+ with context.add_nested_field(name):
97
+ for f in fields(cls):
98
+ # Skip ClassVars and internal fields
99
+ nested_field_name = f.name
100
+ is_computed_only = ResourceAbs.is_computed_only(nested_field_name, cls)
101
+ if is_computed_only or nested_field_name in context.ignored_names:
102
+ continue
103
+ tf_type = python_type_to_terraform_type(f, hints[nested_field_name], context)
104
+ is_required = ResourceAbs.is_required(nested_field_name, cls)
105
+ if default_value := ResourceAbs.default_hcl_string(nested_field_name, cls):
106
+ context.add_default(nested_field_name, default_value)
107
+ lines.append(f" {nested_field_name} = optional({tf_type}, {default_value})")
108
+ elif is_required:
109
+ lines.append(f" {nested_field_name} = {tf_type}")
110
+ else:
111
+ lines.append(f" {nested_field_name} = optional({tf_type})")
112
+ lines.append("})")
113
+ return "\n".join(lines)
114
+
115
+
116
+ def generate_module_variables(
117
+ python_module: ResourceTypePythonModule, resource_config: ResourceGenConfig
118
+ ) -> tuple[str, str]:
119
+ base_resource = python_module.resource
120
+ assert base_resource is not None, f"{python_module} does not have a resource"
121
+ skipped_names_in_resource_ext = set(python_module.base_field_names)
122
+ return generate_resource_variables(base_resource, resource_config), generate_resource_variables(
123
+ python_module.resource_ext, resource_config, skipped_names_in_resource_ext
124
+ )
125
+
126
+
127
+ def generate_resource_variables(
128
+ resource: type[ResourceAbs] | None, resource_config: ResourceGenConfig, extra_skipped: set[str] | None = None
129
+ ) -> str:
130
+ extra_skipped = extra_skipped or set()
131
+ required_variables = resource_config.required_variables
132
+ if resource is None:
133
+ return ""
134
+ out = []
135
+ hints = get_type_hints(resource)
136
+ ignored_names = (
137
+ resource_config.skip_variables_extra
138
+ | resource.COMPUTED_ONLY_ATTRIBUTES
139
+ | getattr(resource, ResourceAbs.SKIP_VARIABLES_NAME, set())
140
+ | extra_skipped
141
+ )
142
+ if resource_config.use_single_variable:
143
+ context = DefaultValueContext(field_path=[], ignored_names=ignored_names)
144
+ tf_type = dataclass_to_object_type(resource_config.name, resource, context)
145
+ return format_tf_content(f'''variable "{resource_config.name}" {{
146
+ type = {tf_type}
147
+ }}\n''')
148
+ for f in fields(resource): # type: ignore
149
+ field_name = f.name
150
+ if field_name.isupper() or field_name in ignored_names:
151
+ continue
152
+ context = DefaultValueContext(field_path=[field_name])
153
+ tf_type = python_type_to_terraform_type(f, hints[field_name], context)
154
+ default_line = f"\n default = {context.final_str}" if field_name not in required_variables else ""
155
+ nullable_line = "\n nullable = true" if field_name not in required_variables else ""
156
+ out.append(f'''variable "{field_name}" {{
157
+ type = {tf_type}{nullable_line}{default_line}
158
+ }}\n''')
159
+ return format_tf_content("\n".join(out))
@@ -0,0 +1,10 @@
1
+ from pathlib import Path
2
+ from atlas_init.tf_ext.provider_schema import get_providers_tf
3
+ from zero_3rdparty.file_utils import ensure_parents_write_text
4
+
5
+
6
+ def dump_versions_tf(module_path: Path, skip_python: bool = False, minimal: bool = False) -> Path:
7
+ provider_path = module_path / "versions.tf"
8
+ if not provider_path.exists():
9
+ ensure_parents_write_text(provider_path, get_providers_tf(skip_python=skip_python, minimal=minimal))
10
+ return provider_path
@@ -0,0 +1,106 @@
1
+ from __future__ import annotations
2
+ from typing import Self
3
+
4
+ from model_lib import Entity
5
+ from pydantic import Field, RootModel, model_validator
6
+
7
+ from atlas_init.tf_ext.tf_dep import AtlasGraph
8
+
9
+ _emojii_list = [
10
+ "1️⃣",
11
+ "2️⃣",
12
+ "3️⃣",
13
+ "4️⃣",
14
+ "5️⃣",
15
+ "6️⃣",
16
+ "7️⃣",
17
+ "8️⃣",
18
+ "9️⃣",
19
+ "🔟",
20
+ "1️⃣1️⃣",
21
+ "1️⃣2️⃣",
22
+ ]
23
+ _emoji_counter = 0
24
+
25
+
26
+ def choose_next_emoji() -> str:
27
+ global _emoji_counter
28
+ emoji = _emojii_list[_emoji_counter]
29
+ _emoji_counter += 1
30
+ return emoji
31
+
32
+
33
+ class ModuleState(Entity):
34
+ resource_types: set[str] = Field(default_factory=set, description="Set of resource types in the module.")
35
+
36
+
37
+ def default_allowed_multi_parents() -> set[str]:
38
+ return {
39
+ "mongodbatlas_project",
40
+ }
41
+
42
+
43
+ class ModuleConfig(Entity):
44
+ name: str = Field(..., description="Name of the module.")
45
+ root_resource_types: list[str] = Field(..., description="List of root resource types for the module.")
46
+ force_include_children: list[str] = Field(
47
+ default_factory=list, description="List of resource types that should always be included as children."
48
+ )
49
+ emojii: str = Field(init=False, default_factory=choose_next_emoji)
50
+ allowed_multi_parents: set[str] = Field(
51
+ default_factory=default_allowed_multi_parents,
52
+ description="Set of parents that a child resource type can have in addition to the root_resource_type.",
53
+ )
54
+ allow_external_dependencies: bool = Field(
55
+ default=False, description="Whether to allow external dependencies for the module."
56
+ )
57
+ extra_nested_resource_types: list[str] = Field(
58
+ default_factory=list,
59
+ description="List of additional nested resource types that should be included in the module.",
60
+ )
61
+
62
+ state: ModuleState = Field(default_factory=ModuleState, description="Internal state of the module.")
63
+
64
+ @model_validator(mode="after")
65
+ def update_state(self) -> Self:
66
+ self.state.resource_types.update(self.root_resource_types)
67
+ return self
68
+
69
+ @property
70
+ def tree_label(self) -> str:
71
+ return f"{self.emojii} {self.name}"
72
+
73
+ def include_child(self, child: str, atlas_graph: AtlasGraph) -> bool:
74
+ if child in atlas_graph.deprecated_resource_types:
75
+ return False
76
+ if child in self.force_include_children or child in self.extra_nested_resource_types:
77
+ self.state.resource_types.add(child)
78
+ return True
79
+ has_external_dependencies = len(atlas_graph.external_parents.get(child, [])) > 0
80
+ if self.allow_external_dependencies and has_external_dependencies:
81
+ has_external_dependencies = False
82
+ is_a_parent = bool(atlas_graph.parent_child_edges.get(child))
83
+ extra_parents = (
84
+ set(atlas_graph.all_parents(child))
85
+ - self.allowed_multi_parents
86
+ - set(self.root_resource_types)
87
+ - set(self.extra_nested_resource_types)
88
+ )
89
+ has_extra_parents = len(extra_parents) > 0
90
+ if has_external_dependencies or is_a_parent or has_extra_parents:
91
+ return False
92
+ self.state.resource_types.add(child)
93
+ return True
94
+
95
+
96
+ class ModuleConfigs(RootModel[dict[str, ModuleConfig]]):
97
+ def module_emoji_prefix(self, resource_type: str) -> str:
98
+ """Get the emoji prefix for a resource type based on its module."""
99
+ return next(
100
+ (
101
+ module_config.emojii
102
+ for module_config in self.root.values()
103
+ if resource_type in module_config.state.resource_types
104
+ ),
105
+ "",
106
+ )