atlas-init 0.4.5__py3-none-any.whl → 0.7.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 (83) hide show
  1. atlas_init/__init__.py +1 -1
  2. atlas_init/cli.py +2 -0
  3. atlas_init/cli_args.py +19 -1
  4. atlas_init/cli_cfn/cfn_parameter_finder.py +59 -51
  5. atlas_init/cli_cfn/example.py +8 -16
  6. atlas_init/cli_helper/go.py +6 -10
  7. atlas_init/cli_root/mms_released.py +46 -0
  8. atlas_init/cli_tf/app.py +3 -84
  9. atlas_init/cli_tf/ci_tests.py +585 -0
  10. atlas_init/cli_tf/codegen/__init__.py +0 -0
  11. atlas_init/cli_tf/codegen/models.py +97 -0
  12. atlas_init/cli_tf/codegen/openapi_minimal.py +74 -0
  13. atlas_init/cli_tf/github_logs.py +7 -94
  14. atlas_init/cli_tf/go_test_run.py +395 -130
  15. atlas_init/cli_tf/go_test_summary.py +589 -10
  16. atlas_init/cli_tf/go_test_tf_error.py +388 -0
  17. atlas_init/cli_tf/hcl/modifier.py +14 -12
  18. atlas_init/cli_tf/hcl/modifier2.py +207 -0
  19. atlas_init/cli_tf/mock_tf_log.py +1 -1
  20. atlas_init/cli_tf/{schema_v2_api_parsing.py → openapi.py} +101 -19
  21. atlas_init/cli_tf/schema_v2.py +43 -1
  22. atlas_init/crud/__init__.py +0 -0
  23. atlas_init/crud/mongo_client.py +115 -0
  24. atlas_init/crud/mongo_dao.py +296 -0
  25. atlas_init/crud/mongo_utils.py +239 -0
  26. atlas_init/html_out/__init__.py +0 -0
  27. atlas_init/html_out/md_export.py +143 -0
  28. atlas_init/repos/go_sdk.py +12 -3
  29. atlas_init/repos/path.py +110 -7
  30. atlas_init/sdk_ext/__init__.py +0 -0
  31. atlas_init/sdk_ext/go.py +102 -0
  32. atlas_init/sdk_ext/typer_app.py +18 -0
  33. atlas_init/settings/config.py +3 -6
  34. atlas_init/settings/env_vars.py +18 -2
  35. atlas_init/settings/env_vars_generated.py +2 -0
  36. atlas_init/settings/interactive2.py +134 -0
  37. atlas_init/tf/.terraform.lock.hcl +59 -59
  38. atlas_init/tf/always.tf +5 -5
  39. atlas_init/tf/main.tf +3 -3
  40. atlas_init/tf/modules/aws_kms/aws_kms.tf +1 -1
  41. atlas_init/tf/modules/aws_s3/provider.tf +2 -1
  42. atlas_init/tf/modules/aws_vpc/provider.tf +2 -1
  43. atlas_init/tf/modules/cfn/cfn.tf +0 -8
  44. atlas_init/tf/modules/cfn/kms.tf +5 -5
  45. atlas_init/tf/modules/cfn/provider.tf +7 -0
  46. atlas_init/tf/modules/cfn/variables.tf +1 -1
  47. atlas_init/tf/modules/cloud_provider/cloud_provider.tf +1 -1
  48. atlas_init/tf/modules/cloud_provider/provider.tf +2 -1
  49. atlas_init/tf/modules/cluster/cluster.tf +31 -31
  50. atlas_init/tf/modules/cluster/provider.tf +2 -1
  51. atlas_init/tf/modules/encryption_at_rest/provider.tf +2 -1
  52. atlas_init/tf/modules/federated_vars/federated_vars.tf +2 -3
  53. atlas_init/tf/modules/federated_vars/provider.tf +2 -1
  54. atlas_init/tf/modules/project_extra/project_extra.tf +1 -10
  55. atlas_init/tf/modules/project_extra/provider.tf +8 -0
  56. atlas_init/tf/modules/stream_instance/provider.tf +8 -0
  57. atlas_init/tf/modules/stream_instance/stream_instance.tf +0 -9
  58. atlas_init/tf/modules/vpc_peering/provider.tf +10 -0
  59. atlas_init/tf/modules/vpc_peering/vpc_peering.tf +0 -10
  60. atlas_init/tf/modules/vpc_privatelink/versions.tf +2 -1
  61. atlas_init/tf/outputs.tf +1 -0
  62. atlas_init/tf/providers.tf +1 -1
  63. atlas_init/tf/variables.tf +7 -7
  64. atlas_init/tf_ext/__init__.py +0 -0
  65. atlas_init/tf_ext/__main__.py +3 -0
  66. atlas_init/tf_ext/api_call.py +325 -0
  67. atlas_init/tf_ext/args.py +17 -0
  68. atlas_init/tf_ext/constants.py +3 -0
  69. atlas_init/tf_ext/models.py +106 -0
  70. atlas_init/tf_ext/paths.py +126 -0
  71. atlas_init/tf_ext/settings.py +39 -0
  72. atlas_init/tf_ext/tf_dep.py +324 -0
  73. atlas_init/tf_ext/tf_modules.py +394 -0
  74. atlas_init/tf_ext/tf_vars.py +173 -0
  75. atlas_init/tf_ext/typer_app.py +24 -0
  76. atlas_init/typer_app.py +4 -8
  77. {atlas_init-0.4.5.dist-info → atlas_init-0.7.0.dist-info}/METADATA +8 -4
  78. atlas_init-0.7.0.dist-info/RECORD +138 -0
  79. atlas_init-0.7.0.dist-info/entry_points.txt +5 -0
  80. atlas_init-0.4.5.dist-info/RECORD +0 -105
  81. atlas_init-0.4.5.dist-info/entry_points.txt +0 -2
  82. {atlas_init-0.4.5.dist-info → atlas_init-0.7.0.dist-info}/WHEEL +0 -0
  83. {atlas_init-0.4.5.dist-info → atlas_init-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -2,11 +2,11 @@ from collections import defaultdict
2
2
  from pathlib import Path
3
3
 
4
4
  import requests
5
- from model_lib import parse_model
5
+ from model_lib import Entity, parse_model
6
6
 
7
- from atlas_init.cli_tf.debug_logs_test_data import ApiSpecPath
7
+ from atlas_init.cli_tf.debug_logs_test_data import ApiSpecPath, find_normalized_path
8
8
  from atlas_init.cli_tf.schema import logger
9
- from atlas_init.cli_tf.schema_v2_api_parsing import OpenapiSchema
9
+ from atlas_init.cli_tf.openapi import OpenapiSchema
10
10
 
11
11
 
12
12
  def go_sdk_breaking_changes(repo_path: Path, go_sdk_rel_path: str = "../atlas-sdk-go") -> Path:
@@ -21,6 +21,15 @@ def api_spec_path_transformed(sdk_repo_path: Path) -> Path:
21
21
  return sdk_repo_path / "openapi/atlas-api-transformed.yaml"
22
22
 
23
23
 
24
+ class ApiSpecPaths(Entity):
25
+ method_paths: dict[str, list[ApiSpecPath]]
26
+
27
+ def normalize_path(self, method: str, path: str) -> str:
28
+ if path.startswith("/api/atlas/v1.0"):
29
+ return ""
30
+ return find_normalized_path(path, self.method_paths[method]).path
31
+
32
+
24
33
  def parse_api_spec_paths(api_spec_path: Path) -> dict[str, list[ApiSpecPath]]:
25
34
  model = parse_model(api_spec_path, t=OpenapiSchema)
26
35
  paths: dict[str, list[ApiSpecPath]] = defaultdict(list)
atlas_init/repos/path.py CHANGED
@@ -1,4 +1,9 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ import re
4
+ from collections import defaultdict
1
5
  from collections.abc import Callable
6
+ from dataclasses import dataclass
2
7
  from enum import StrEnum
3
8
  from functools import lru_cache
4
9
  from pathlib import Path
@@ -8,6 +13,7 @@ from git import Repo as _GitRepo
8
13
 
9
14
  from atlas_init.settings.path import current_dir, repo_path_rel_path
10
15
 
16
+ logger = logging.getLogger(__name__)
11
17
  GH_OWNER_TERRAFORM_PROVIDER_MONGODBATLAS = "mongodb/terraform-provider-mongodbatlas"
12
18
  GH_OWNER_MONGODBATLAS_CLOUDFORMATION_RESOURCES = "mongodb/mongodbatlas-cloudformation-resources"
13
19
  _KNOWN_OWNER_PROJECTS = {
@@ -59,10 +65,10 @@ def is_resource_call(repo_path: Path) -> Callable[[Path], bool]:
59
65
 
60
66
 
61
67
  def resource_dir(repo_path: Path, full_path: Path) -> Path:
62
- dir_name = resource_name(repo_path, full_path)
63
- if not dir_name:
68
+ if dir_name := resource_name(repo_path, full_path):
69
+ return resource_root(repo_path) / dir_name
70
+ else:
64
71
  raise ValueError(f"no resource name for {full_path}")
65
- return resource_root(repo_path) / dir_name
66
72
 
67
73
 
68
74
  class Repo(StrEnum):
@@ -116,10 +122,10 @@ def resource_name(repo_path: Path, full_path: Path) -> str:
116
122
  is_resource = is_resource_call(repo_path)
117
123
  if not root.exists():
118
124
  raise ValueError(f"no resource root found for {repo_path}")
119
- for parent in [full_path, *full_path.parents]:
120
- if parent.parent == root and is_resource(parent):
121
- return parent.name
122
- return ""
125
+ return next(
126
+ (parent.name for parent in [full_path, *full_path.parents] if parent.parent == root and is_resource(parent)),
127
+ "",
128
+ )
123
129
 
124
130
 
125
131
  def find_paths(assert_repo: Repo | None = None) -> ResourcePaths:
@@ -153,3 +159,100 @@ def find_go_mod_dir(repo_path: Path):
153
159
  return go_mod.parent
154
160
  msg = "go.mod not found or more than 1 level deep"
155
161
  raise ValueError(msg)
162
+
163
+
164
+ def find_test_names(file: Path, prefix: str = "Test") -> list[str]:
165
+ test_names = []
166
+ with file.open("r") as f:
167
+ for line in f:
168
+ if line.startswith(f"func {prefix}"):
169
+ test_name = line.split("(")[0].strip().removeprefix("func ")
170
+ test_names.append(test_name)
171
+ return sorted(test_names)
172
+
173
+
174
+ class MultipleResourceNames(ValueError):
175
+ def __init__(self, names: list[str]):
176
+ super().__init__(f"multiple resource names found: {names}")
177
+ self.names = names
178
+
179
+
180
+ def find_tf_resource_name_in_test(path: Path, provider_prefix: str = "mongodbatlas_") -> str:
181
+ candidates: set[str] = {
182
+ match.group(1) for match in re.finditer(rf"=\s\"{provider_prefix}([a-zA-Z0-9_]+)\.?", path.read_text())
183
+ }
184
+ if len(candidates) > 1:
185
+ pkg_name = path.parent.name
186
+ for candidate in candidates:
187
+ if candidate.replace("_", "") == pkg_name:
188
+ return candidate
189
+ logger.warning(f"multiple resource names found in {path}: {candidates}")
190
+ raise MultipleResourceNames(sorted(candidates))
191
+ return candidates.pop() if candidates else ""
192
+
193
+
194
+ def find_pkg_test_names(pkg_path: Path, prefix: str = "Test") -> list[str]:
195
+ test_names = []
196
+ for test_file in pkg_path.glob("*_test.go"):
197
+ test_names.extend(find_test_names(test_file, prefix))
198
+ return sorted(test_names)
199
+
200
+
201
+ def terraform_resource_test_names(
202
+ repo_path: Path, prefix: str = "Test", package_path: str = "internal/service"
203
+ ) -> dict[str, list[str]]:
204
+ """find all test names in the given package path"""
205
+ pkg_path = terraform_package_path(repo_path, package_path)
206
+ resource_dirs, _ = find_resource_dirs(pkg_path)
207
+ test_names = defaultdict(list)
208
+ for name, pkg_dir in resource_dirs.items():
209
+ for test_file in pkg_dir.glob("*_test.go"):
210
+ test_names[name].extend(find_test_names(test_file, prefix))
211
+ return test_names
212
+
213
+
214
+ def terraform_resources(repo_path: Path, package_path: str = "internal/service") -> list[TFResoure]:
215
+ pkg_path = terraform_package_path(repo_path, package_path)
216
+ resource_dirs, _ = find_resource_dirs(pkg_path)
217
+ resources = []
218
+ for name, pkg_dir in resource_dirs.items():
219
+ test_names = find_pkg_test_names(pkg_dir)
220
+ resources.append(TFResoure(name=name, package_rel_path=str(pkg_dir.relative_to(repo_path)), tests=test_names))
221
+ return resources
222
+
223
+
224
+ def terraform_package_path(repo_path: Path, package_path: str = "internal/service"):
225
+ pkg_path = repo_path / package_path
226
+ if not pkg_path.exists():
227
+ raise ValueError(f"package path not found: {pkg_path}")
228
+ return pkg_path
229
+
230
+
231
+ def find_resource_dirs(pkg_path: Path) -> tuple[dict[str, Path], list[Path]]:
232
+ resource_dirs: dict[str, Path] = {}
233
+ non_resource_dirs: list[Path] = []
234
+ for pkg_dir in pkg_path.iterdir():
235
+ if not pkg_dir.is_dir():
236
+ continue
237
+ if pkg_dir.name == "testdata":
238
+ continue
239
+ found = False
240
+ for test_file in pkg_dir.glob("*_test.go"):
241
+ try:
242
+ if name := find_tf_resource_name_in_test(test_file):
243
+ resource_dirs[name] = pkg_dir
244
+ found = True
245
+ except MultipleResourceNames as e:
246
+ for name in e.names:
247
+ resource_dirs[name] = pkg_dir
248
+ found = True
249
+ if not found:
250
+ non_resource_dirs.append(pkg_dir)
251
+ return resource_dirs, non_resource_dirs
252
+
253
+
254
+ @dataclass
255
+ class TFResoure:
256
+ name: str
257
+ package_rel_path: str
258
+ tests: list[str]
File without changes
@@ -0,0 +1,102 @@
1
+ from contextlib import suppress
2
+ import logging
3
+ from pathlib import Path
4
+ from ask_shell import confirm, run_and_wait
5
+ from model_lib import dump, parse_model
6
+ import typer
7
+ from zero_3rdparty.file_utils import clean_dir, copy, ensure_parents_write_text
8
+ from atlas_init.cli_args import ParsedPaths, option_sdk_repo_path, option_mms_repo_path
9
+ from atlas_init.cli_tf.openapi import OpenapiSchema
10
+
11
+ _go_mod_line = "replace go.mongodb.org/atlas-sdk/v20250312005 v20250312005.0.0 => ../atlas-sdk-go"
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def go(
16
+ mms_path_str: str = option_mms_repo_path,
17
+ sdk_repo_path_str: str = option_sdk_repo_path,
18
+ mms_branch: str = typer.Option("master", "--mms-branch", help="Branch to use for mms"),
19
+ skip_mms_openapi: bool = typer.Option(
20
+ False, "-smms", "--skip-mms-openapi", help="Skip mms openapi generation, use existing file instead"
21
+ ),
22
+ ):
23
+ paths = ParsedPaths.from_strings(sdk_repo_path_str=sdk_repo_path_str, mms_path=mms_path_str)
24
+ mms_path = paths.mms_repo_path
25
+ assert mms_path, "mms_path is required"
26
+ sdk_path = paths.sdk_repo_path
27
+ assert sdk_path, "sdk_path is required"
28
+ openapi_path = safe_openapi_path(mms_path) if skip_mms_openapi else generate_openapi_spec(mms_path, mms_branch)
29
+ openapi_path = transform_openapi(openapi_path, sdk_path / "openapi/openapi-mms.yaml")
30
+ generate_go_sdk(sdk_path, openapi_path)
31
+ confirm(f"Have you remembered to add to your go.mod file: {_go_mod_line}")
32
+
33
+
34
+ def transform_openapi(old: Path, dest_path: Path) -> Path:
35
+ api_spec = parse_model(old, t=OpenapiSchema)
36
+ new_api_spec = api_spec.model_dump()
37
+ for path in api_spec.paths.keys():
38
+ for method_name, method in api_spec.methods_with_name(path):
39
+ responses = method.get("responses", {})
40
+ for code, multi_responses in responses.items():
41
+ with suppress(AlreadySingleVersion):
42
+ new_api_spec["paths"][path][method_name]["responses"][code]["content"] = use_a_single_version(
43
+ multi_responses, api_spec, path
44
+ )
45
+ if request_body := method.get("requestBody", {}):
46
+ with suppress(AlreadySingleVersion):
47
+ new_api_spec["paths"][path][method_name]["requestBody"]["content"] = use_a_single_version(
48
+ request_body, api_spec, path
49
+ )
50
+ dest_yaml = dump(new_api_spec, "yaml")
51
+ ensure_parents_write_text(dest_path, dest_yaml)
52
+ return dest_path
53
+
54
+
55
+ class AlreadySingleVersion(Exception):
56
+ pass
57
+
58
+
59
+ def use_a_single_version(multi_content: dict, api_spec: OpenapiSchema, path: str) -> dict[str, dict]:
60
+ if api_versions := api_spec._unpack_schema_versions(multi_content):
61
+ if len(api_versions) > 1:
62
+ latest_version = max(api_versions)
63
+ last_header = f"application/vnd.atlas.{latest_version}+json"
64
+ old_content = multi_content["content"]
65
+ assert last_header in old_content, f"failed to find {last_header} for {path} in {old_content.keys()}"
66
+ return {last_header: old_content[last_header]}
67
+ raise AlreadySingleVersion
68
+
69
+
70
+ def generate_openapi_spec(mms_path: Path, mms_branch: str) -> Path:
71
+ run_and_wait(f"git stash && git checkout {mms_branch}", cwd=mms_path)
72
+ bazelisk_bin_run = run_and_wait("mise which bazelisk", cwd=mms_path)
73
+ bazelisk_bin = bazelisk_bin_run.stdout_one_line
74
+ assert Path(bazelisk_bin).exists(), f"not found {bazelisk_bin}"
75
+ openapi_run = run_and_wait(f"{bazelisk_bin} run //server:mms-openapi", cwd=mms_path, print_prefix="mms-openapi")
76
+ assert openapi_run.clean_complete, f"failed to run {openapi_run}"
77
+ return safe_openapi_path(mms_path)
78
+
79
+
80
+ def safe_openapi_path(mms_path: Path) -> Path:
81
+ openapi_path = mms_path / "server/openapi/services/openapi-mms.json"
82
+ assert openapi_path.exists(), f"not found {openapi_path}"
83
+ return openapi_path
84
+
85
+
86
+ def generate_go_sdk(repo_path: Path, openapi_path: Path) -> None:
87
+ SDK_FOLDER = repo_path / "admin"
88
+ clean_dir(SDK_FOLDER, recreate=True)
89
+ generate_script = repo_path / "tools/scripts/generate.sh"
90
+ assert generate_script.exists(), f"not found {generate_script}"
91
+ openapi_folder = repo_path / "openapi"
92
+ openapi_dest_path = openapi_folder / openapi_path.name
93
+ if openapi_path != openapi_dest_path:
94
+ copy(openapi_path, openapi_dest_path)
95
+ generate_env = {
96
+ "OPENAPI_FOLDER": str(openapi_folder),
97
+ "OPENAPI_FILE_NAME": openapi_path.name,
98
+ "SDK_FOLDER": str(SDK_FOLDER),
99
+ }
100
+ run_and_wait(f"{generate_script}", cwd=repo_path / "tools", env=generate_env, print_prefix="go sdk create")
101
+ mockery_script = repo_path / "tools/scripts/generate_mocks.sh"
102
+ run_and_wait(f"{mockery_script}", cwd=repo_path / "tools", print_prefix="go sdk mockery")
@@ -0,0 +1,18 @@
1
+ from ask_shell import configure_logging
2
+ from typer import Typer
3
+
4
+
5
+ def typer_main():
6
+ from atlas_init.sdk_ext import go
7
+
8
+ app = Typer(
9
+ name="sdk-ext",
10
+ help="SDK extension commands for Atlas Init",
11
+ )
12
+ app.command(name="go")(go.go)
13
+ configure_logging(app)
14
+ app()
15
+
16
+
17
+ if __name__ == "__main__":
18
+ typer_main()
@@ -12,7 +12,7 @@ from typing import Any
12
12
  from model_lib import Entity, IgnoreFalsy
13
13
  from pydantic import Field, model_validator
14
14
 
15
- from atlas_init.repos.path import as_repo_alias, go_package_prefix, owner_project_name, package_glob
15
+ from atlas_init.repos.path import as_repo_alias, find_test_names, go_package_prefix, owner_project_name, package_glob
16
16
 
17
17
  logger = logging.getLogger(__name__)
18
18
 
@@ -95,11 +95,8 @@ class TestSuite(IgnoreFalsy):
95
95
  for package in packages:
96
96
  pkg_name = f"{go_package_prefix(repo_path)}/{package}"
97
97
  for go_file in repo_path.glob(f"{package}/*.go"):
98
- with go_file.open() as f:
99
- for line in f:
100
- if line.startswith(f"func {prefix}"):
101
- test_name = line.split("(")[0].strip().removeprefix("func ")
102
- names[pkg_name][test_name] = go_file.parent
98
+ for name in find_test_names(go_file, prefix):
99
+ names[pkg_name][name] = go_file.parent
103
100
  return names
104
101
 
105
102
  def is_active(self, repo_alias: str, change_paths: Iterable[str]) -> bool:
@@ -9,7 +9,7 @@ from typing import Any, NamedTuple, TypeVar
9
9
 
10
10
  from model_lib import StaticSettings, parse_payload
11
11
  from pydantic import BaseModel, ValidationError, field_validator
12
- from zero_3rdparty import iter_utils
12
+ from zero_3rdparty import iter_utils, str_utils
13
13
 
14
14
  from atlas_init.settings.config import (
15
15
  AtlasInitConfig,
@@ -54,6 +54,7 @@ class AtlasInitSettings(StaticSettings):
54
54
  atlas_init_tf_src_path: Path = DEFAULT_TF_SRC_PATH # /tf directory of repo
55
55
  atlas_init_tf_schema_config_path: Path = DEFAULT_ATLAS_INIT_SCHEMA_CONFIG_PATH # /terraform.yaml
56
56
  atlas_init_schema_out_path: Path | None = None # override this for the generated schema
57
+ atlas_init_static_html_path: Path | None = None
57
58
 
58
59
  atlas_init_cfn_profile: str = ""
59
60
  atlas_init_cfn_region: str = ""
@@ -64,6 +65,9 @@ class AtlasInitSettings(StaticSettings):
64
65
 
65
66
  non_interactive: bool = False
66
67
 
68
+ mongo_database: str = "atlas_init"
69
+ mongo_url: str = "mongodb://user:pass@localhost:27017?retryWrites=true&w=majority&authSource=admin"
70
+
67
71
  @property
68
72
  def is_interactive(self) -> bool:
69
73
  return not self.non_interactive
@@ -133,6 +137,17 @@ class AtlasInitSettings(StaticSettings):
133
137
  def github_ci_summary_dir(self) -> Path:
134
138
  return self.cache_root / "github_ci_summary"
135
139
 
140
+ def github_ci_summary_path(self, summary_name: str) -> Path:
141
+ return self.github_ci_summary_dir / str_utils.ensure_suffix(summary_name, ".md")
142
+
143
+ def github_ci_summary_details_path(self, summary_name: str, test_name: str) -> Path:
144
+ return self.github_ci_summary_path(summary_name).parent / self.github_ci_summary_details_rel_path(
145
+ summary_name, test_name
146
+ )
147
+
148
+ def github_ci_summary_details_rel_path(self, summary_name: str, test_name: str) -> str:
149
+ return f"{summary_name.removesuffix('.md')}_details/{test_name}.md"
150
+
136
151
  @property
137
152
  def go_test_logs_dir(self) -> Path:
138
153
  return self.cache_root / "go_test_logs"
@@ -234,6 +249,7 @@ def find_missing_env_vars(required_env_vars: list[str], manual_env_vars: dict[st
234
249
 
235
250
  def init_settings(
236
251
  *settings_classes: type[BaseModel],
252
+ skip_ambiguous_check: bool = False,
237
253
  ) -> AtlasInitSettings:
238
254
  settings = AtlasInitSettings.from_env()
239
255
  profile_env_vars = settings.manual_env_vars
@@ -241,7 +257,7 @@ def init_settings(
241
257
  if vscode_env_vars.exists():
242
258
  profile_env_vars |= load_dotenv(vscode_env_vars)
243
259
  required_env_vars = collect_required_env_vars(list(settings_classes))
244
- ambiguous = detect_ambiguous_env_vars(profile_env_vars)
260
+ ambiguous = [] if skip_ambiguous_check else detect_ambiguous_env_vars(profile_env_vars)
245
261
  missing_env_vars = find_missing_env_vars(required_env_vars, profile_env_vars)
246
262
 
247
263
  if ambiguous:
@@ -63,3 +63,5 @@ class RealmSettings(_EnvVarsGenerated):
63
63
 
64
64
  class AtlasSettingsWithProject(AtlasSettings):
65
65
  MONGODB_ATLAS_PROJECT_ID: str
66
+ MONGODB_ATLAS_PROJECT_OWNER_ID: str = ""
67
+ MONGODB_ATLAS_USER_EMAIL: str = ""
@@ -0,0 +1,134 @@
1
+ """Inspired by: https://github.com/tmbo/questionary/blob/master/tests/utils.py"""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable, TypeVar
5
+
6
+ from prompt_toolkit.input.defaults import create_pipe_input
7
+ from prompt_toolkit.output import DummyOutput
8
+ from questionary import Question, checkbox
9
+ from questionary import confirm as _confirm
10
+ from questionary import select as _select
11
+ from questionary import text as _text
12
+
13
+ T = TypeVar("T")
14
+ TypedAsk = Callable[[Question, type[T]], T]
15
+
16
+ _question_asker: TypedAsk = lambda q, _: q.ask() # noqa: E731
17
+
18
+
19
+ def confirm(prompt_text: str, *, default: bool | None = None) -> bool:
20
+ if default is None:
21
+ return _question_asker(_confirm(prompt_text), bool)
22
+ return _question_asker(_confirm(prompt_text, default=default), bool)
23
+
24
+
25
+ def select_list_multiple(
26
+ prompt_text: str,
27
+ choices: list[str],
28
+ default: list[str] | None = None,
29
+ ) -> list[str]:
30
+ assert choices, "choices must not be empty"
31
+ default = default or []
32
+ return _question_asker(checkbox(prompt_text, choices=choices), list[str]) or default
33
+
34
+
35
+ def text(
36
+ prompt_text: str,
37
+ default: str = "",
38
+ ) -> str:
39
+ return _question_asker(_text(prompt_text, default=default), str)
40
+
41
+
42
+ T = TypeVar("T")
43
+
44
+
45
+ def select_dict(
46
+ prompt_text: str,
47
+ choices: dict[str, T],
48
+ default: str | None = None,
49
+ ) -> T:
50
+ assert choices, "choices must not be empty"
51
+ selection = _question_asker(_select(prompt_text, default=default, choices=list(choices)), str)
52
+ return choices[selection]
53
+
54
+
55
+ StrT = TypeVar("StrT", bound=str)
56
+
57
+
58
+ def select_list(
59
+ prompt_text: str,
60
+ choices: list[StrT],
61
+ default: StrT | None = None,
62
+ ) -> StrT:
63
+ assert choices, "choices must not be empty"
64
+ return _question_asker(_select(prompt_text, default=default, choices=choices), str)
65
+
66
+
67
+ class KeyInput:
68
+ DOWN = "\x1b[B"
69
+ UP = "\x1b[A"
70
+ LEFT = "\x1b[D"
71
+ RIGHT = "\x1b[C"
72
+ ENTER = "\r"
73
+ ESCAPE = "\x1b"
74
+ CONTROLC = "\x03"
75
+ CONTROLN = "\x0e"
76
+ CONTROLP = "\x10"
77
+ BACK = "\x7f"
78
+ SPACE = " "
79
+ TAB = "\x09"
80
+ ONE = "1"
81
+ TWO = "2"
82
+ THREE = "3"
83
+
84
+
85
+ @dataclass
86
+ class question_patcher:
87
+ responses: list[str]
88
+ next_response: int = 0
89
+
90
+ def __enter__(self):
91
+ global _question_asker
92
+ self._old_patcher = _question_asker
93
+ _question_asker = self.ask_question
94
+ return self
95
+
96
+ def __exit__(self, exc_type, exc_val, exc_tb):
97
+ global _question_patcher
98
+ _question_patcher = self._old_patcher
99
+
100
+ def ask_question(self, q: Question, response_type: type[T]) -> T:
101
+ q.application.output = DummyOutput()
102
+
103
+ def run(inp) -> T:
104
+ try:
105
+ input_response = self.responses[self.next_response]
106
+ except IndexError:
107
+ raise ValueError(
108
+ f"Not enough responses provided. Expected {len(self.responses)}, got {self.next_response + 1} questions."
109
+ )
110
+ self.next_response += 1
111
+ inp.send_text(input_response + KeyInput.ENTER + "\r")
112
+ q.application.output = DummyOutput()
113
+ q.application.input = inp
114
+ return q.ask()
115
+
116
+ with create_pipe_input() as inp:
117
+ return run(inp)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ print(select_list("Select an option:", ["Option 1", "Option 2", "Option 3"])) # noqa: T201
122
+ print( # noqa: T201
123
+ select_dict(
124
+ "Select an option:",
125
+ {"Option 1": "1", "Option 2": "2", "Option 3": "3"},
126
+ default="Option 3",
127
+ )
128
+ )
129
+ print(confirm("Can you confirm?", default=True)) # noqa: T201
130
+ print(confirm("Can you confirm?", default=False)) # noqa: T201
131
+ print( # noqa: T201
132
+ select_list_multiple("Select options:", ["Option 1", "Option 2", "Option 3"], ["Option 1"])
133
+ )
134
+ print(text("Enter your name:", default="John Doe")) # noqa: T201