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.
- atlas_init/__init__.py +1 -1
- atlas_init/cli.py +2 -0
- atlas_init/cli_args.py +19 -1
- atlas_init/cli_cfn/cfn_parameter_finder.py +59 -51
- atlas_init/cli_cfn/example.py +8 -16
- atlas_init/cli_helper/go.py +6 -10
- atlas_init/cli_root/mms_released.py +46 -0
- atlas_init/cli_tf/app.py +3 -84
- atlas_init/cli_tf/ci_tests.py +585 -0
- atlas_init/cli_tf/codegen/__init__.py +0 -0
- atlas_init/cli_tf/codegen/models.py +97 -0
- atlas_init/cli_tf/codegen/openapi_minimal.py +74 -0
- atlas_init/cli_tf/github_logs.py +7 -94
- atlas_init/cli_tf/go_test_run.py +395 -130
- atlas_init/cli_tf/go_test_summary.py +589 -10
- atlas_init/cli_tf/go_test_tf_error.py +388 -0
- atlas_init/cli_tf/hcl/modifier.py +14 -12
- atlas_init/cli_tf/hcl/modifier2.py +207 -0
- atlas_init/cli_tf/mock_tf_log.py +1 -1
- atlas_init/cli_tf/{schema_v2_api_parsing.py → openapi.py} +101 -19
- atlas_init/cli_tf/schema_v2.py +43 -1
- atlas_init/crud/__init__.py +0 -0
- atlas_init/crud/mongo_client.py +115 -0
- atlas_init/crud/mongo_dao.py +296 -0
- atlas_init/crud/mongo_utils.py +239 -0
- atlas_init/html_out/__init__.py +0 -0
- atlas_init/html_out/md_export.py +143 -0
- atlas_init/repos/go_sdk.py +12 -3
- atlas_init/repos/path.py +110 -7
- atlas_init/sdk_ext/__init__.py +0 -0
- atlas_init/sdk_ext/go.py +102 -0
- atlas_init/sdk_ext/typer_app.py +18 -0
- atlas_init/settings/config.py +3 -6
- atlas_init/settings/env_vars.py +18 -2
- atlas_init/settings/env_vars_generated.py +2 -0
- atlas_init/settings/interactive2.py +134 -0
- atlas_init/tf/.terraform.lock.hcl +59 -59
- atlas_init/tf/always.tf +5 -5
- atlas_init/tf/main.tf +3 -3
- atlas_init/tf/modules/aws_kms/aws_kms.tf +1 -1
- atlas_init/tf/modules/aws_s3/provider.tf +2 -1
- atlas_init/tf/modules/aws_vpc/provider.tf +2 -1
- atlas_init/tf/modules/cfn/cfn.tf +0 -8
- atlas_init/tf/modules/cfn/kms.tf +5 -5
- atlas_init/tf/modules/cfn/provider.tf +7 -0
- atlas_init/tf/modules/cfn/variables.tf +1 -1
- atlas_init/tf/modules/cloud_provider/cloud_provider.tf +1 -1
- atlas_init/tf/modules/cloud_provider/provider.tf +2 -1
- atlas_init/tf/modules/cluster/cluster.tf +31 -31
- atlas_init/tf/modules/cluster/provider.tf +2 -1
- atlas_init/tf/modules/encryption_at_rest/provider.tf +2 -1
- atlas_init/tf/modules/federated_vars/federated_vars.tf +2 -3
- atlas_init/tf/modules/federated_vars/provider.tf +2 -1
- atlas_init/tf/modules/project_extra/project_extra.tf +1 -10
- atlas_init/tf/modules/project_extra/provider.tf +8 -0
- atlas_init/tf/modules/stream_instance/provider.tf +8 -0
- atlas_init/tf/modules/stream_instance/stream_instance.tf +0 -9
- atlas_init/tf/modules/vpc_peering/provider.tf +10 -0
- atlas_init/tf/modules/vpc_peering/vpc_peering.tf +0 -10
- atlas_init/tf/modules/vpc_privatelink/versions.tf +2 -1
- atlas_init/tf/outputs.tf +1 -0
- atlas_init/tf/providers.tf +1 -1
- atlas_init/tf/variables.tf +7 -7
- atlas_init/tf_ext/__init__.py +0 -0
- atlas_init/tf_ext/__main__.py +3 -0
- atlas_init/tf_ext/api_call.py +325 -0
- atlas_init/tf_ext/args.py +17 -0
- atlas_init/tf_ext/constants.py +3 -0
- atlas_init/tf_ext/models.py +106 -0
- atlas_init/tf_ext/paths.py +126 -0
- atlas_init/tf_ext/settings.py +39 -0
- atlas_init/tf_ext/tf_dep.py +324 -0
- atlas_init/tf_ext/tf_modules.py +394 -0
- atlas_init/tf_ext/tf_vars.py +173 -0
- atlas_init/tf_ext/typer_app.py +24 -0
- atlas_init/typer_app.py +4 -8
- {atlas_init-0.4.5.dist-info → atlas_init-0.7.0.dist-info}/METADATA +8 -4
- atlas_init-0.7.0.dist-info/RECORD +138 -0
- atlas_init-0.7.0.dist-info/entry_points.txt +5 -0
- atlas_init-0.4.5.dist-info/RECORD +0 -105
- atlas_init-0.4.5.dist-info/entry_points.txt +0 -2
- {atlas_init-0.4.5.dist-info → atlas_init-0.7.0.dist-info}/WHEEL +0 -0
- {atlas_init-0.4.5.dist-info → atlas_init-0.7.0.dist-info}/licenses/LICENSE +0 -0
atlas_init/repos/go_sdk.py
CHANGED
@@ -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.
|
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
|
63
|
-
|
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
|
-
|
120
|
-
if parent.parent == root and is_resource(parent)
|
121
|
-
|
122
|
-
|
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
|
atlas_init/sdk_ext/go.py
ADDED
@@ -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()
|
atlas_init/settings/config.py
CHANGED
@@ -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
|
-
|
99
|
-
|
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:
|
atlas_init/settings/env_vars.py
CHANGED
@@ -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:
|
@@ -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
|