atlas-init 0.8.0__tar.gz → 0.9.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. {atlas_init-0.8.0 → atlas_init-0.9.0}/PKG-INFO +1 -1
  2. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/__init__.py +1 -1
  3. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_root/trigger.py +1 -1
  4. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/rich_utils.py +22 -0
  5. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/gen_examples.py +4 -2
  6. atlas_init-0.8.0/atlas_init/tf_ext/gen_module_readme.py → atlas_init-0.9.0/atlas_init/tf_ext/gen_readme.py +46 -26
  7. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/gen_resource_variables.py +5 -2
  8. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/models.py +27 -1
  9. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/models_module.py +8 -3
  10. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/tf_dep.py +69 -22
  11. atlas_init-0.9.0/atlas_init/tf_ext/tf_example_readme.py +392 -0
  12. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/tf_mod_gen.py +4 -6
  13. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/tf_modules.py +3 -1
  14. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/typer_app.py +2 -1
  15. {atlas_init-0.8.0 → atlas_init-0.9.0}/.gitignore +0 -0
  16. {atlas_init-0.8.0 → atlas_init-0.9.0}/LICENSE +0 -0
  17. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/__main__.py +0 -0
  18. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/atlas_init.yaml +0 -0
  19. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli.py +0 -0
  20. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_args.py +0 -0
  21. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_cfn/__init__.py +0 -0
  22. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_cfn/app.py +0 -0
  23. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_cfn/aws.py +0 -0
  24. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_cfn/cfn_parameter_finder.py +0 -0
  25. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_cfn/contract.py +0 -0
  26. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_cfn/example.py +0 -0
  27. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_cfn/files.py +0 -0
  28. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_helper/__init__.py +0 -0
  29. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_helper/go.py +0 -0
  30. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_helper/run.py +0 -0
  31. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_helper/run_manager.py +0 -0
  32. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_helper/sdk.py +0 -0
  33. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_helper/sdk_auto_changes.py +0 -0
  34. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_helper/tf_runner.py +0 -0
  35. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_root/__init__.py +0 -0
  36. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_root/go_test.py +0 -0
  37. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_root/mms_released.py +0 -0
  38. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/__init__.py +0 -0
  39. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/app.py +0 -0
  40. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/changelog.py +0 -0
  41. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/ci_tests.py +0 -0
  42. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/codegen/__init__.py +0 -0
  43. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/codegen/models.py +0 -0
  44. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/codegen/openapi_minimal.py +0 -0
  45. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/debug_logs.py +0 -0
  46. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/debug_logs_test_data.py +0 -0
  47. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/debug_logs_test_data_package_config.py +0 -0
  48. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/example_update.py +0 -0
  49. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/github_logs.py +0 -0
  50. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/go_test_run.py +0 -0
  51. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/go_test_summary.py +0 -0
  52. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/go_test_tf_error.py +0 -0
  53. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/hcl/__init__.py +0 -0
  54. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/hcl/cli.py +0 -0
  55. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/hcl/cluster_mig.py +0 -0
  56. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/hcl/modifier.py +0 -0
  57. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/hcl/modifier2.py +0 -0
  58. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/hcl/parser.py +0 -0
  59. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/log_clean.py +0 -0
  60. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/mock_tf_log.py +0 -0
  61. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/openapi.py +0 -0
  62. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema.py +0 -0
  63. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_go_parser.py +0 -0
  64. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_inspection.py +0 -0
  65. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_table.py +0 -0
  66. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_table_models.py +0 -0
  67. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_v2.py +0 -0
  68. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_v2_sdk.py +0 -0
  69. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_v3.py +0 -0
  70. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_v3_sdk.py +0 -0
  71. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_v3_sdk_base.py +0 -0
  72. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cli_tf/schema_v3_sdk_create.py +0 -0
  73. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cloud/__init__.py +0 -0
  74. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/cloud/aws.py +0 -0
  75. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/crud/__init__.py +0 -0
  76. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/crud/mongo_client.py +0 -0
  77. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/crud/mongo_dao.py +0 -0
  78. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/crud/mongo_utils.py +0 -0
  79. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/html_out/__init__.py +0 -0
  80. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/html_out/md_export.py +0 -0
  81. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/humps.py +0 -0
  82. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/repos/__init__.py +0 -0
  83. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/repos/cfn.py +0 -0
  84. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/repos/go_sdk.py +0 -0
  85. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/repos/path.py +0 -0
  86. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/sdk_ext/__init__.py +0 -0
  87. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/sdk_ext/go.py +0 -0
  88. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/sdk_ext/typer_app.py +0 -0
  89. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/__init__.py +0 -0
  90. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/config.py +0 -0
  91. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/env_vars.py +0 -0
  92. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/env_vars_generated.py +0 -0
  93. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/env_vars_modules.py +0 -0
  94. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/interactive.py +0 -0
  95. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/interactive2.py +0 -0
  96. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/settings/path.py +0 -0
  97. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/terraform.yaml +0 -0
  98. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/.terraform.lock.hcl +0 -0
  99. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/always.tf +0 -0
  100. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/main.tf +0 -0
  101. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/aws_kms/aws_kms.tf +0 -0
  102. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/aws_kms/provider.tf +0 -0
  103. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/aws_s3/aws_s3.tf +0 -0
  104. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/aws_s3/provider.tf +0 -0
  105. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/aws_vars/aws_vars.tf +0 -0
  106. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/aws_vpc/aws_vpc.tf +0 -0
  107. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/aws_vpc/provider.tf +0 -0
  108. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cfn/assume_role_services.yaml +0 -0
  109. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cfn/cfn.tf +0 -0
  110. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cfn/kms.tf +0 -0
  111. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cfn/provider.tf +0 -0
  112. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cfn/resource_actions.yaml +0 -0
  113. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cfn/variables.tf +0 -0
  114. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cloud_provider/cloud_provider.tf +0 -0
  115. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cloud_provider/provider.tf +0 -0
  116. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cluster/cluster.tf +0 -0
  117. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/cluster/provider.tf +0 -0
  118. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/encryption_at_rest/main.tf +0 -0
  119. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/encryption_at_rest/provider.tf +0 -0
  120. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/federated_vars/federated_vars.tf +0 -0
  121. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/federated_vars/provider.tf +0 -0
  122. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/project_extra/project_extra.tf +0 -0
  123. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/project_extra/provider.tf +0 -0
  124. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/stream_instance/provider.tf +0 -0
  125. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/stream_instance/stream_instance.tf +0 -0
  126. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/vpc_peering/provider.tf +0 -0
  127. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/vpc_peering/vpc_peering.tf +0 -0
  128. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/vpc_privatelink/atlas-privatelink.tf +0 -0
  129. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/vpc_privatelink/variables.tf +0 -0
  130. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/modules/vpc_privatelink/versions.tf +0 -0
  131. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/outputs.tf +0 -0
  132. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/providers.tf +0 -0
  133. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf/variables.tf +0 -0
  134. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/__init__.py +0 -0
  135. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/__main__.py +0 -0
  136. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/api_call.py +0 -0
  137. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/args.py +0 -0
  138. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/constants.py +0 -0
  139. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/gen_resource_main.py +0 -0
  140. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/gen_resource_output.py +0 -0
  141. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/gen_versions.py +0 -0
  142. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/newres.py +0 -0
  143. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/paths.py +0 -0
  144. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/plan_diffs.py +0 -0
  145. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/provider_schema.py +0 -0
  146. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/py_gen.py +0 -0
  147. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/schema_to_dataclass.py +0 -0
  148. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/settings.py +0 -0
  149. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/tf_desc_gen.py +0 -0
  150. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/tf_desc_update.py +0 -0
  151. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/tf_mod_gen_provider.py +0 -0
  152. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/tf_ext/tf_vars.py +0 -0
  153. {atlas_init-0.8.0 → atlas_init-0.9.0}/atlas_init/typer_app.py +0 -0
  154. {atlas_init-0.8.0 → atlas_init-0.9.0}/pyproject.toml +0 -0
  155. {atlas_init-0.8.0 → atlas_init-0.9.0}/readme.md +0 -0
@@ -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,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