atlas-init 0.4.2__py3-none-any.whl → 0.4.4__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 (34) hide show
  1. atlas_init/__init__.py +3 -7
  2. atlas_init/cli.py +2 -1
  3. atlas_init/cli_cfn/app.py +3 -5
  4. atlas_init/cli_cfn/cfn_parameter_finder.py +1 -28
  5. atlas_init/cli_cfn/contract.py +5 -3
  6. atlas_init/cli_cfn/example.py +8 -6
  7. atlas_init/cli_helper/go.py +18 -14
  8. atlas_init/cli_helper/tf_runner.py +14 -11
  9. atlas_init/cli_root/trigger.py +21 -8
  10. atlas_init/cli_tf/app.py +1 -1
  11. atlas_init/cli_tf/debug_logs.py +4 -4
  12. atlas_init/cli_tf/example_update.py +3 -3
  13. atlas_init/cli_tf/github_logs.py +4 -16
  14. atlas_init/cli_tf/hcl/modifier.py +115 -14
  15. atlas_init/cli_tf/mock_tf_log.py +4 -3
  16. atlas_init/cli_tf/schema_v2.py +2 -2
  17. atlas_init/cli_tf/schema_v2_api_parsing.py +3 -3
  18. atlas_init/cli_tf/schema_v3.py +2 -2
  19. atlas_init/cli_tf/schema_v3_sdk_base.py +1 -1
  20. atlas_init/settings/env_vars.py +119 -142
  21. atlas_init/settings/env_vars_generated.py +40 -9
  22. atlas_init/settings/env_vars_modules.py +71 -0
  23. atlas_init/settings/path.py +4 -10
  24. atlas_init/typer_app.py +3 -3
  25. {atlas_init-0.4.2.dist-info → atlas_init-0.4.4.dist-info}/METADATA +6 -5
  26. {atlas_init-0.4.2.dist-info → atlas_init-0.4.4.dist-info}/RECORD +29 -32
  27. atlas_init-0.4.4.dist-info/licenses/LICENSE +21 -0
  28. atlas_init/cli_tf/example_update_test/test_update_example.tf +0 -23
  29. atlas_init/cli_tf/example_update_test.py +0 -96
  30. atlas_init/cli_tf/hcl/modifier_test/test_process_variables_output_.tf +0 -25
  31. atlas_init/cli_tf/hcl/modifier_test/test_process_variables_variable_.tf +0 -24
  32. atlas_init/cli_tf/hcl/modifier_test.py +0 -95
  33. {atlas_init-0.4.2.dist-info → atlas_init-0.4.4.dist-info}/WHEEL +0 -0
  34. {atlas_init-0.4.2.dist-info → atlas_init-0.4.4.dist-info}/entry_points.txt +0 -0
@@ -2,6 +2,7 @@ import logging
2
2
  from collections import defaultdict
3
3
  from copy import deepcopy
4
4
  from pathlib import Path
5
+ from typing import Callable
5
6
 
6
7
  import hcl2
7
8
  from lark import Token, Tree, UnexpectedToken
@@ -51,6 +52,8 @@ def update_description(tree: Tree, new_descriptions: dict[str, str], existing_na
51
52
  def token_name(token: Token | Tree) -> str:
52
53
  if isinstance(token, Token):
53
54
  return token.value.strip('"')
55
+ if isinstance(token, Tree) and token.data == "identifier":
56
+ return token.children[0].value.strip('"') # type: ignore
54
57
  err_msg = f"unexpected token type {type(token)} for token name"
55
58
  raise ValueError(err_msg)
56
59
 
@@ -102,31 +105,46 @@ def create_description_attribute(description_value: str) -> Tree:
102
105
  return Tree(Token("RULE", "attribute"), children)
103
106
 
104
107
 
105
- def process_descriptions(
108
+ def process_generic(
106
109
  node: Tree,
107
- name_updates: dict[str, str],
108
- existing_names: dict[str, list[str]],
110
+ tree_match: Callable[[Tree], bool],
111
+ tree_call: Callable[[Tree], Tree],
109
112
  depth=0,
110
- *,
111
- block_type: str,
112
- ) -> Tree:
113
+ ):
113
114
  new_children = []
114
115
  logger.debug(f"[{depth}] (tree)\t|", " " * depth, node.data)
115
116
  for child in node.children:
116
117
  if isinstance(child, Tree):
117
- if is_block_type(child, block_type):
118
- child = update_description( # noqa: PLW2901
119
- child, name_updates, existing_names
120
- )
121
- new_children.append(
122
- process_descriptions(child, name_updates, existing_names, depth + 1, block_type=block_type)
123
- )
118
+ if tree_match(child):
119
+ child = tree_call(child)
120
+ new_children.append(process_generic(child, tree_match, tree_call, depth + 1))
124
121
  else:
125
122
  new_children.append(process_token(child, depth + 1))
126
-
127
123
  return Tree(node.data, new_children)
128
124
 
129
125
 
126
+ def process_descriptions(
127
+ node: Tree,
128
+ name_updates: dict[str, str],
129
+ existing_names: dict[str, list[str]],
130
+ depth=0,
131
+ *,
132
+ block_type: str,
133
+ ) -> Tree:
134
+ def tree_match(tree: Tree) -> bool:
135
+ return is_block_type(tree, block_type)
136
+
137
+ def tree_call(tree: Tree) -> Tree:
138
+ return update_description(tree, name_updates, existing_names)
139
+
140
+ return process_generic(
141
+ node,
142
+ tree_match,
143
+ tree_call,
144
+ depth=depth,
145
+ )
146
+
147
+
130
148
  def update_descriptions(tf_path: Path, new_names: dict[str, str], block_type: str) -> tuple[str, dict[str, list[str]]]:
131
149
  try:
132
150
  tree = hcl2.parses(tf_path.read_text()) # type: ignore
@@ -142,3 +160,86 @@ def update_descriptions(tf_path: Path, new_names: dict[str, str], block_type: st
142
160
  )
143
161
  new_tf = hcl2.writes(new_tree) # type: ignore
144
162
  return new_tf, existing_descriptions
163
+
164
+
165
+ def _block_name_body(tree: Tree) -> tuple[str, Tree]:
166
+ try:
167
+ _, name_token, body = tree.children
168
+ name = token_name(name_token)
169
+ except (IndexError, AttributeError) as e:
170
+ raise ValueError("unexpected block structure") from e
171
+ return name, body
172
+
173
+
174
+ def _read_attribute(tree_body: Tree, attribute_name: str) -> Tree | None:
175
+ for attribute in tree_body.children:
176
+ if not isinstance(attribute, Tree):
177
+ continue
178
+ if attribute.data != "attribute":
179
+ continue
180
+ attr_identifier, _, attr_value = attribute.children
181
+ if token_name(attr_identifier.children[0]) != attribute_name:
182
+ continue
183
+ return attr_value
184
+
185
+
186
+ def _is_object(tree_body: Tree) -> bool:
187
+ if not isinstance(tree_body, Tree):
188
+ return False
189
+ if len(tree_body.children) != 1:
190
+ return False
191
+ if not isinstance(tree_body.children[0], Tree):
192
+ return False
193
+ return tree_body.children[0].data == "object"
194
+
195
+
196
+ def _read_object_elems(tree_body: Tree) -> list[Tree]:
197
+ object_elements = []
198
+ for obj_child in tree_body.children[0].children:
199
+ if not isinstance(obj_child, Tree):
200
+ continue
201
+ if obj_child.data != "object_elem":
202
+ continue
203
+ object_elements.append(obj_child)
204
+ return object_elements
205
+
206
+
207
+ def _read_object_elem_key(tree_body: Tree) -> str:
208
+ name_tree, _, _ = tree_body.children
209
+ return token_name(name_tree.children[0])
210
+
211
+
212
+ def read_block_attribute_object_keys(tf_path: Path, block_type: str, block_name: str, block_key: str) -> list[str]:
213
+ try:
214
+ tree = hcl2.parses(tf_path.read_text()) # type: ignore
215
+ except UnexpectedToken as e:
216
+ logger.warning(f"failed to parse {tf_path}: {e}")
217
+ return []
218
+ env_vars = []
219
+
220
+ def extract_env_vars(tree: Tree) -> bool:
221
+ if not is_block_type(tree, block_type):
222
+ return False
223
+ name, body = _block_name_body(tree)
224
+ if name != block_name:
225
+ return False
226
+ attribute_value = _read_attribute(body, block_key)
227
+ if not attribute_value:
228
+ return False
229
+ if not _is_object(attribute_value):
230
+ return False
231
+ object_elements = _read_object_elems(attribute_value)
232
+ for obj_elem in object_elements:
233
+ key = _read_object_elem_key(obj_elem)
234
+ env_vars.append(key)
235
+ return False
236
+
237
+ def tree_call(tree: Tree) -> Tree:
238
+ return tree
239
+
240
+ process_generic(
241
+ tree,
242
+ extract_env_vars,
243
+ tree_call,
244
+ )
245
+ return env_vars
@@ -33,7 +33,7 @@ from atlas_init.repos.go_sdk import (
33
33
  download_admin_api,
34
34
  parse_api_spec_paths,
35
35
  )
36
- from atlas_init.settings.path import DEFAULT_DOWNLOADS_DIR
36
+ from atlas_init.settings.env_vars import init_settings
37
37
 
38
38
  logger = logging.getLogger(__name__)
39
39
 
@@ -149,7 +149,7 @@ def is_cache_up_to_date(cache_path: Path, cache_ttl: int) -> bool:
149
149
  if cache_path.exists():
150
150
  modified_ts = file_utils.file_modified_time(cache_path)
151
151
  if modified_ts > time.time() - cache_ttl:
152
- logger.info(f"using cached admin api: {cache_path} downloaded {time.time()-modified_ts:.0f}s ago")
152
+ logger.info(f"using cached admin api: {cache_path} downloaded {time.time() - modified_ts:.0f}s ago")
153
153
  return True
154
154
  return False
155
155
 
@@ -164,7 +164,8 @@ def resolve_admin_api_path(sdk_repo_path_str: str, sdk_branch: str, admin_api_pa
164
164
  assert sdk_repo_path.exists(), f"not found sdk_repo_path={sdk_repo_path}"
165
165
  resolved_admin_api_path = api_spec_path_transformed(sdk_repo_path)
166
166
  else:
167
- resolved_admin_api_path = DEFAULT_DOWNLOADS_DIR / "atlas-api-transformed.yaml"
167
+ settings = init_settings()
168
+ resolved_admin_api_path = settings.atlas_atlas_api_transformed_yaml
168
169
  if not is_cache_up_to_date(resolved_admin_api_path, 3600):
169
170
  download_admin_api(resolved_admin_api_path, sdk_branch)
170
171
  assert resolved_admin_api_path.exists(), f"unable to resolve admin_api_path={resolved_admin_api_path}"
@@ -466,8 +466,8 @@ def generate_go_attribute_schema_lines(
466
466
  attr_name = attr.tf_name
467
467
  lines = [indent(line_indent, f'"{attr_name}": {attribute_header(attr)}{{')]
468
468
  if desc := attr.description or attr.is_nested and (desc := schema.ref_resource(attr.schema_ref).description):
469
- lines.append(indent(line_indent + 1, f'Description: "{desc.replace('\n', '\\n')}",'))
470
- lines.append(indent(line_indent + 1, f'MarkdownDescription: "{desc.replace('\n', '\\n')}",'))
469
+ lines.append(indent(line_indent + 1, f'Description: "{desc.replace("\n", "\\n")}",'))
470
+ lines.append(indent(line_indent + 1, f'MarkdownDescription: "{desc.replace("\n", "\\n")}",'))
471
471
  if attr.is_required:
472
472
  lines.append(indent(line_indent + 1, "Required: true,"))
473
473
  if attr.is_optional:
@@ -32,9 +32,9 @@ def api_spec_text_changes(schema: SchemaV2, api_spec_parsed: OpenapiSchema) -> O
32
32
  if name.startswith(prefix):
33
33
  schema_to_update.pop(name)
34
34
  name_no_prefix = name.removeprefix(prefix)
35
- assert (
36
- name_no_prefix not in schema_to_update
37
- ), f"removed {prefix} from {name} in schema but {name_no_prefix} already exists"
35
+ assert name_no_prefix not in schema_to_update, (
36
+ f"removed {prefix} from {name} in schema but {name_no_prefix} already exists"
37
+ )
38
38
  schema_to_update[name_no_prefix] = value
39
39
  openapi_yaml = dump(api_spec_parsed, "yaml")
40
40
  for prefix in openapi_changes.schema_prefix_removal:
@@ -210,12 +210,12 @@ class Schema(BaseModelLocal):
210
210
 
211
211
 
212
212
  class Resource(BaseModelLocal):
213
- schema: Schema # pyright: ignore
213
+ local_schema: Schema = Field(alias="schema")
214
214
  name: SnakeCaseString
215
215
 
216
216
  @property
217
217
  def use_timeout(self) -> bool:
218
- return any(a.timeouts for a in self.schema.attributes)
218
+ return any(a.timeouts for a in self.local_schema.attributes)
219
219
 
220
220
 
221
221
  ResourceSchemaV3 = Resource
@@ -41,7 +41,7 @@ def schema_attributes(root: object) -> list[Attribute]:
41
41
 
42
42
  @schema_attributes.register
43
43
  def _resource_attributes(root: Resource) -> list[Attribute]:
44
- return root.schema.attributes
44
+ return root.local_schema.attributes
45
45
 
46
46
 
47
47
  @schema_attributes.register
@@ -7,11 +7,10 @@ from functools import cached_property
7
7
  from pathlib import Path
8
8
  from typing import Any, NamedTuple, TypeVar
9
9
 
10
- from model_lib import parse_payload
11
- from pydantic import ValidationError, field_validator, model_validator
12
- from pydantic_settings import BaseSettings, SettingsConfigDict
10
+ from model_lib import StaticSettings, parse_payload
11
+ from pydantic import BaseModel, ValidationError, field_validator
12
+ from zero_3rdparty import iter_utils
13
13
 
14
- from atlas_init.cloud.aws import AwsRegion
15
14
  from atlas_init.settings.config import (
16
15
  AtlasInitConfig,
17
16
  TestSuite,
@@ -19,12 +18,10 @@ from atlas_init.settings.config import (
19
18
  from atlas_init.settings.config import (
20
19
  active_suites as config_active_suites,
21
20
  )
22
- from atlas_init.settings.env_vars_generated import AtlasSettings
23
21
  from atlas_init.settings.path import (
24
- DEFAULT_CONFIG_PATH,
25
- DEFAULT_PROFILES_PATH,
26
- DEFAULT_SCHEMA_CONFIG_PATH,
27
- DEFAULT_TF_PATH,
22
+ DEFAULT_ATLAS_INIT_CONFIG_PATH,
23
+ DEFAULT_ATLAS_INIT_SCHEMA_CONFIG_PATH,
24
+ DEFAULT_TF_SRC_PATH,
28
25
  dump_dotenv,
29
26
  load_dotenv,
30
27
  repo_path_rel_path,
@@ -33,66 +30,59 @@ from atlas_init.settings.path import (
33
30
  logger = logging.getLogger(__name__)
34
31
  ENV_PREFIX = "ATLAS_INIT_"
35
32
  DEFAULT_PROFILE = "default"
33
+ DEFAULT_PROJECT_NAME = "atlas-init"
36
34
  ENV_S3_PROFILE_BUCKET = f"{ENV_PREFIX}S3_PROFILE_BUCKET"
37
35
  ENV_PROJECT_NAME = f"{ENV_PREFIX}PROJECT_NAME"
38
36
  ENV_PROFILE = f"{ENV_PREFIX}PROFILE"
39
- ENV_PROFILES_PATH = f"{ENV_PREFIX}PROFILES_PATH"
40
37
  ENV_TEST_SUITES = f"{ENV_PREFIX}TEST_SUITES"
41
38
  ENV_CLIPBOARD_COPY = f"{ENV_PREFIX}CLIPBOARD_COPY"
42
- REQUIRED_FIELDS = [
43
- "MONGODB_ATLAS_ORG_ID",
44
- "MONGODB_ATLAS_PRIVATE_KEY",
45
- "MONGODB_ATLAS_PUBLIC_KEY",
46
- ]
47
39
  FILENAME_ENV_MANUAL = ".env-manual"
48
40
  T = TypeVar("T")
49
41
 
50
42
 
51
- class ExternalSettings(AtlasSettings):
52
- model_config = SettingsConfigDict(env_prefix="", extra="ignore")
43
+ def read_from_env(env_key: str, default: str = "") -> str:
44
+ return next(
45
+ (os.environ[name] for name in [env_key, env_key.lower(), env_key.upper()] if name in os.environ),
46
+ default,
47
+ )
53
48
 
54
- TF_CLI_CONFIG_FILE: str = ""
55
- AWS_PROFILE: str = ""
56
- AWS_REGION: AwsRegion = "us-east-1"
57
- non_interactive: bool = False
58
49
 
59
- @property
60
- def realm_url(self) -> str:
61
- assert not self.is_mongodbgov_cloud, "realm_url is not supported for mongodbgov cloud"
62
- if "cloud-dev." in self.MONGODB_ATLAS_BASE_URL:
63
- return "https://services.cloud-dev.mongodb.com/"
64
- return "https://services.cloud.mongodb.com/"
50
+ class AtlasInitSettings(StaticSettings):
51
+ atlas_init_profile: str = DEFAULT_PROFILE # override this for different env, e.g. dev, prod
52
+ atlas_init_project_name: str = DEFAULT_PROJECT_NAME # used in the atlas cloud
53
+ atlas_init_config_path: Path = DEFAULT_ATLAS_INIT_CONFIG_PATH # /atlas_init.yaml
54
+ atlas_init_tf_src_path: Path = DEFAULT_TF_SRC_PATH # /tf directory of repo
55
+ atlas_init_tf_schema_config_path: Path = DEFAULT_ATLAS_INIT_SCHEMA_CONFIG_PATH # /terraform.yaml
56
+ atlas_init_schema_out_path: Path | None = None # override this for the generated schema
57
+
58
+ atlas_init_cfn_profile: str = ""
59
+ atlas_init_cfn_region: str = ""
60
+ atlas_init_cfn_use_kms_key: bool = False
61
+ atlas_init_cliboard_copy: str = ""
62
+ atlas_init_test_suites: str = ""
63
+ atlas_init_s3_profile_bucket: str = ""
64
+
65
+ non_interactive: bool = False
65
66
 
66
67
  @property
67
68
  def is_interactive(self) -> bool:
68
69
  return not self.non_interactive
69
70
 
70
71
  @property
71
- def is_mongodbgov_cloud(self) -> bool:
72
- return "mongodbgov" in self.MONGODB_ATLAS_BASE_URL
72
+ def profiles_path(self) -> Path:
73
+ return self.static_root / "profiles"
73
74
 
75
+ @property
76
+ def project_name(self) -> str:
77
+ return self.atlas_init_project_name
74
78
 
75
- def read_from_env(env_key: str, default: str = "") -> str:
76
- for name in [env_key, env_key.lower(), env_key.upper()]:
77
- if name in os.environ:
78
- return os.environ[name]
79
- return default
80
-
81
-
82
- class AtlasInitPaths(BaseSettings):
83
- model_config = SettingsConfigDict(env_prefix=ENV_PREFIX)
84
-
85
- profile: str = DEFAULT_PROFILE
86
- config_path: Path = DEFAULT_CONFIG_PATH
87
- tf_path: Path = DEFAULT_TF_PATH
88
- profiles_path: Path = DEFAULT_PROFILES_PATH
89
- tf_schema_config_path: Path = DEFAULT_SCHEMA_CONFIG_PATH
90
- schema_out_path: Path | None = None
91
- s3_profile_bucket: str = ""
79
+ @property
80
+ def profile(self) -> str:
81
+ return self.atlas_init_profile
92
82
 
93
83
  @property
94
84
  def schema_out_path_computed(self) -> Path:
95
- return self.schema_out_path or self.profile_dir / "schema"
85
+ return self.atlas_init_schema_out_path or self.static_root / "schema"
96
86
 
97
87
  @property
98
88
  def profile_dir(self) -> Path:
@@ -135,8 +125,24 @@ class AtlasInitPaths(BaseSettings):
135
125
  def tf_outputs_path(self) -> Path:
136
126
  return self.profile_dir / "tf_outputs.json"
137
127
 
138
- def load_env_vars(self, path: Path) -> dict[str, str]:
139
- return load_dotenv(path)
128
+ @property
129
+ def github_ci_run_logs(self) -> Path:
130
+ return self.cache_root / "github_ci_run_logs"
131
+
132
+ @property
133
+ def github_ci_summary_dir(self) -> Path:
134
+ return self.cache_root / "github_ci_summary"
135
+
136
+ @property
137
+ def go_test_logs_dir(self) -> Path:
138
+ return self.cache_root / "go_test_logs"
139
+
140
+ @property
141
+ def atlas_atlas_api_transformed_yaml(self) -> Path:
142
+ return self.cache_root / "atlas_api_transformed.yaml"
143
+
144
+ def cfn_region(self, default: str) -> str:
145
+ return self.atlas_init_cfn_region or default
140
146
 
141
147
  def load_env_vars_full(self) -> dict[str, str]:
142
148
  env_path = self.env_vars_vs_code
@@ -149,7 +155,7 @@ class AtlasInitPaths(BaseSettings):
149
155
 
150
156
  def env_vars_cls(self, t: type[T], *, path: Path | None = None) -> T:
151
157
  path = path or self.env_vars_vs_code
152
- env_vars = self.load_env_vars(path) if path.exists() else {}
158
+ env_vars = load_dotenv(path) if path.exists() else {}
153
159
  return t(**env_vars)
154
160
 
155
161
  def load_profile_manual_env_vars(self, *, skip_os_update: bool = False) -> dict[str, str]:
@@ -161,77 +167,25 @@ class AtlasInitPaths(BaseSettings):
161
167
  if new_updates := {k: v for k, v in manual_env_vars.items() if k not in os.environ}:
162
168
  logger.info(f"loading manual env-vars {','.join(new_updates)}")
163
169
  os.environ.update(new_updates)
164
- else:
165
- logger.warning(f"no {self.env_file_manual} exists")
166
170
  return manual_env_vars
167
171
 
168
172
  def include_extra_env_vars_in_vscode(self, extra_env_vars: dict[str, str]) -> None:
169
173
  extra_name = ", ".join(extra_env_vars.keys())
170
- original_env_vars = self.load_env_vars(self.env_vars_vs_code)
174
+ original_env_vars = load_dotenv(self.env_vars_vs_code)
171
175
  new_env_vars = original_env_vars | extra_env_vars
172
176
  dump_dotenv(self.env_vars_vs_code, new_env_vars)
173
177
  logger.info(f"done {self.env_vars_vs_code} updated with {extra_name} env-vars ✅")
174
178
 
175
-
176
- class EnvVarsCheck(NamedTuple):
177
- missing: list[str]
178
- ambiguous: list[str]
179
-
180
-
181
- class AtlasInitSettings(AtlasInitPaths, ExternalSettings):
182
- model_config = SettingsConfigDict(env_prefix=ENV_PREFIX)
183
-
184
- cfn_profile: str = ""
185
- cfn_region: str = ""
186
- cfn_use_kms_key: bool = False
187
- project_name: str = ""
188
-
189
- cliboard_copy: str = ""
190
- test_suites: str = ""
191
-
192
- @classmethod
193
- def check_env_vars(
194
- cls,
195
- profile: str = DEFAULT_PROFILE,
196
- required_env_vars: list[str] | None = None,
197
- ) -> EnvVarsCheck:
198
- required_env_vars = required_env_vars or []
199
- path_settings = AtlasInitPaths(profile=profile)
200
- manual_env_vars = path_settings.manual_env_vars
201
- ambiguous: list[str] = []
202
- for env_name, manual_value in manual_env_vars.items():
203
- env_value = read_from_env(env_name)
204
- if env_value and manual_value != env_value:
205
- ambiguous.append(env_name)
206
- missing_env_vars = sorted(
207
- env_name
208
- for env_name in REQUIRED_FIELDS + required_env_vars
209
- if read_from_env(env_name) == "" and env_name not in manual_env_vars
210
- )
211
- return EnvVarsCheck(missing=missing_env_vars, ambiguous=sorted(ambiguous))
212
-
213
- @classmethod
214
- def safe_settings(cls, profile: str, *, ext_settings: ExternalSettings | None = None) -> AtlasInitSettings:
215
- """side effect of loading manual env-vars and set profile"""
216
- os.environ[ENV_PROFILE] = profile
217
- AtlasInitPaths(profile=profile).load_profile_manual_env_vars()
218
- ext_settings = ext_settings or ExternalSettings() # type: ignore
219
- path_settings = AtlasInitPaths()
220
- return cls(**path_settings.model_dump(), **ext_settings.model_dump())
221
-
222
- @field_validator("test_suites", mode="after")
179
+ @field_validator(ENV_TEST_SUITES.lower(), mode="after")
223
180
  @classmethod
224
181
  def ensure_whitespace_replaced_with_commas(cls, value: str) -> str:
225
182
  return value.strip().replace(" ", ",")
226
183
 
227
- @model_validator(mode="after")
228
- def post_init(self):
229
- self.cfn_region = self.cfn_region or self.AWS_REGION
230
- return self
231
-
232
184
  @cached_property
233
185
  def config(self) -> AtlasInitConfig:
234
- config_path = Path(self.config_path) if self.config_path else DEFAULT_CONFIG_PATH
186
+ config_path = (
187
+ Path(self.atlas_init_config_path) if self.atlas_init_config_path else DEFAULT_ATLAS_INIT_CONFIG_PATH
188
+ )
235
189
  assert config_path.exists(), f"no config path found @ {config_path}"
236
190
  yaml_parsed = parse_payload(config_path)
237
191
  assert isinstance(yaml_parsed, dict), f"config must be a dictionary, got {yaml_parsed}"
@@ -239,30 +193,35 @@ class AtlasInitSettings(AtlasInitPaths, ExternalSettings):
239
193
 
240
194
  @property
241
195
  def test_suites_parsed(self) -> list[str]:
242
- return [t for t in self.test_suites.split(",") if t]
196
+ return [t for t in self.atlas_init_test_suites.split(",") if t]
243
197
 
244
- def tf_vars(self) -> dict[str, Any]:
198
+ def tf_vars(self, default_aws_region: str) -> dict[str, Any]:
245
199
  variables = {}
246
- if self.cfn_profile:
200
+ if self.atlas_init_cfn_profile:
247
201
  variables["cfn_config"] = {
248
- "profile": self.cfn_profile,
249
- "region": self.cfn_region,
250
- "use_kms_key": self.cfn_use_kms_key,
202
+ "profile": self.atlas_init_cfn_profile,
203
+ "region": self.atlas_init_cfn_region or default_aws_region,
204
+ "use_kms_key": self.atlas_init_cfn_use_kms_key,
251
205
  }
252
- if self.s3_profile_bucket:
206
+ if self.atlas_init_s3_profile_bucket:
253
207
  variables["use_aws_s3"] = True
254
208
  return variables
255
209
 
256
210
 
211
+ class EnvVarsCheck(NamedTuple):
212
+ missing: list[str]
213
+ ambiguous: list[str]
214
+
215
+ @property
216
+ def is_ok(self) -> bool:
217
+ return not self.missing and not self.ambiguous
218
+
219
+
257
220
  def active_suites(settings: AtlasInitSettings) -> list[TestSuite]: # type: ignore
258
221
  repo_path, cwd_rel_path = repo_path_rel_path()
259
222
  return config_active_suites(settings.config, repo_path, cwd_rel_path, settings.test_suites_parsed)
260
223
 
261
224
 
262
- _sentinel = object()
263
- PLACEHOLDER_VALUE = "PLACEHOLDER"
264
-
265
-
266
225
  class EnvVarsError(Exception):
267
226
  def __init__(self, missing: list[str], ambiguous: list[str]):
268
227
  self.missing = missing
@@ -273,31 +232,49 @@ class EnvVarsError(Exception):
273
232
  return f"missing: {self.missing}, ambiguous: {self.ambiguous}"
274
233
 
275
234
 
235
+ def collect_required_env_vars(settings_classes: list[type[BaseModel]]) -> list[str]:
236
+ cls_required_env_vars: dict[str, list[str]] = {}
237
+ for cls in settings_classes:
238
+ try:
239
+ cls()
240
+ except ValidationError as error:
241
+ cls_required_env_vars[cls.__name__] = [".".join(str(loc) for loc in e["loc"]) for e in error.errors()]
242
+ return list(iter_utils.flat_map(cls_required_env_vars.values()))
243
+
244
+
245
+ def detect_ambiguous_env_vars(manual_env_vars: dict[str, str]) -> list[str]:
246
+ ambiguous: list[str] = []
247
+ for env_name, manual_value in manual_env_vars.items():
248
+ env_value = read_from_env(env_name)
249
+ if env_value and manual_value != env_value:
250
+ ambiguous.append(env_name)
251
+ return ambiguous
252
+
253
+
254
+ def find_missing_env_vars(required_env_vars: list[str], manual_env_vars: dict[str, str]) -> list[str]:
255
+ return sorted(
256
+ env_name for env_name in required_env_vars if read_from_env(env_name) == "" and env_name not in manual_env_vars
257
+ )
258
+
259
+
276
260
  def init_settings(
277
- required_env_vars: list[str] | object = _sentinel,
278
- *,
279
- non_required: bool = False,
261
+ *settings_classes: type[BaseModel],
280
262
  ) -> AtlasInitSettings:
281
- if required_env_vars is _sentinel:
282
- required_env_vars = [ENV_PROJECT_NAME]
283
- if non_required:
284
- required_env_vars = []
285
- profile = os.getenv("ATLAS_INIT_PROFILE", DEFAULT_PROFILE)
286
- missing_env_vars, ambiguous_env_vars = AtlasInitSettings.check_env_vars(
287
- profile,
288
- required_env_vars=required_env_vars, # type: ignore
289
- )
290
- if missing_env_vars and not non_required:
291
- logger.warning(f"missing env_vars: {missing_env_vars}")
292
- if ambiguous_env_vars:
263
+ settings = AtlasInitSettings.from_env()
264
+ manual_env_vars = settings.manual_env_vars
265
+
266
+ required_env_vars = collect_required_env_vars(list(settings_classes))
267
+ ambiguous = detect_ambiguous_env_vars(manual_env_vars)
268
+ missing_env_vars = find_missing_env_vars(required_env_vars, manual_env_vars)
269
+
270
+ if ambiguous:
293
271
  logger.warning(
294
- f"amiguous env_vars: {ambiguous_env_vars} (specified both in cli/env & in .env-manual file with different values)"
272
+ f"ambiguous env_vars: {ambiguous} (specified both in cli/env & in .env-manual file with different values)"
295
273
  )
296
- ext_settings = None
297
- if non_required and missing_env_vars:
298
- placeholders = {k: PLACEHOLDER_VALUE for k in missing_env_vars}
299
- missing_env_vars = []
300
- ext_settings = ExternalSettings(**placeholders) # type: ignore
301
- if missing_env_vars or ambiguous_env_vars:
302
- raise EnvVarsError(missing_env_vars, ambiguous_env_vars)
303
- return AtlasInitSettings.safe_settings(profile, ext_settings=ext_settings)
274
+ if missing_env_vars or ambiguous:
275
+ raise EnvVarsError(missing_env_vars, ambiguous)
276
+
277
+ settings.load_profile_manual_env_vars()
278
+ for cls in settings_classes:
279
+ cls() # ensure any errors are raised
280
+ return AtlasInitSettings.from_env()