dbt-platform-helper 13.1.1__py3-none-any.whl → 13.1.2__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.

Potentially problematic release.


This version of dbt-platform-helper might be problematic. Click here for more details.

Files changed (29) hide show
  1. dbt_platform_helper/commands/application.py +4 -4
  2. dbt_platform_helper/commands/codebase.py +4 -4
  3. dbt_platform_helper/commands/conduit.py +4 -4
  4. dbt_platform_helper/commands/config.py +7 -5
  5. dbt_platform_helper/commands/copilot.py +12 -391
  6. dbt_platform_helper/commands/environment.py +4 -4
  7. dbt_platform_helper/commands/generate.py +1 -1
  8. dbt_platform_helper/commands/notify.py +4 -4
  9. dbt_platform_helper/commands/pipeline.py +4 -4
  10. dbt_platform_helper/commands/secrets.py +4 -4
  11. dbt_platform_helper/commands/version.py +1 -1
  12. dbt_platform_helper/domain/codebase.py +4 -9
  13. dbt_platform_helper/domain/copilot.py +394 -0
  14. dbt_platform_helper/domain/copilot_environment.py +6 -6
  15. dbt_platform_helper/domain/maintenance_page.py +193 -424
  16. dbt_platform_helper/domain/versioning.py +67 -0
  17. dbt_platform_helper/providers/io.py +14 -0
  18. dbt_platform_helper/providers/load_balancers.py +258 -43
  19. dbt_platform_helper/providers/platform_helper_versioning.py +107 -0
  20. dbt_platform_helper/providers/semantic_version.py +27 -7
  21. dbt_platform_helper/providers/version.py +24 -0
  22. dbt_platform_helper/utils/application.py +14 -0
  23. dbt_platform_helper/utils/files.py +6 -0
  24. dbt_platform_helper/utils/versioning.py +11 -158
  25. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.1.2.dist-info}/METADATA +3 -4
  26. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.1.2.dist-info}/RECORD +29 -26
  27. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.1.2.dist-info}/LICENSE +0 -0
  28. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.1.2.dist-info}/WHEEL +0 -0
  29. {dbt_platform_helper-13.1.1.dist-info → dbt_platform_helper-13.1.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,67 @@
1
+ from dbt_platform_helper.platform_exception import PlatformException
2
+ from dbt_platform_helper.providers.io import ClickIOProvider
3
+ from dbt_platform_helper.providers.platform_helper_versioning import (
4
+ PlatformHelperVersioning,
5
+ )
6
+ from dbt_platform_helper.providers.semantic_version import PlatformHelperVersionStatus
7
+ from dbt_platform_helper.providers.semantic_version import SemanticVersion
8
+ from dbt_platform_helper.utils.files import running_as_installed_package
9
+
10
+
11
+ class PlatformHelperVersionNotFoundException(PlatformException):
12
+ def __init__(self):
13
+ super().__init__(f"""Platform helper version could not be resolved.""")
14
+
15
+
16
+ class RequiredVersion:
17
+ def __init__(self, io=None, platform_helper_versioning=None):
18
+ self.io = io or ClickIOProvider()
19
+ self.platform_helper_versioning = platform_helper_versioning or PlatformHelperVersioning(
20
+ io=self.io
21
+ )
22
+
23
+ def get_required_platform_helper_version(
24
+ self, pipeline: str = None, version_status: PlatformHelperVersionStatus = None
25
+ ) -> str:
26
+ pipeline_version = version_status.pipeline_overrides.get(pipeline)
27
+ version_precedence = [
28
+ pipeline_version,
29
+ version_status.platform_config_default,
30
+ version_status.deprecated_version_file,
31
+ ]
32
+ non_null_version_precedence = [
33
+ f"{v}" if isinstance(v, SemanticVersion) else v for v in version_precedence if v
34
+ ]
35
+
36
+ out = non_null_version_precedence[0] if non_null_version_precedence else None
37
+
38
+ if not out:
39
+ raise PlatformHelperVersionNotFoundException
40
+
41
+ return out
42
+
43
+ def get_required_version(self, pipeline=None):
44
+ version_status = self.platform_helper_versioning.get_status()
45
+ self.io.process_messages(version_status.validate())
46
+ required_version = self.get_required_platform_helper_version(pipeline, version_status)
47
+ self.io.info(required_version)
48
+ return required_version
49
+
50
+ # Used in the generate command
51
+ def check_platform_helper_version_mismatch(self):
52
+ if not running_as_installed_package():
53
+ return
54
+
55
+ version_status = self.platform_helper_versioning.get_status()
56
+ self.io.process_messages(version_status.validate())
57
+
58
+ required_version = SemanticVersion.from_string(
59
+ self.get_required_platform_helper_version(version_status=version_status)
60
+ )
61
+
62
+ if not version_status.local == required_version:
63
+ message = (
64
+ f"WARNING: You are running platform-helper v{version_status.local} against "
65
+ f"v{required_version} specified for the project."
66
+ )
67
+ self.io.warn(message)
@@ -26,6 +26,20 @@ class ClickIOProvider:
26
26
  click.secho(f"Error: {message}", err=True, fg="red")
27
27
  exit(1)
28
28
 
29
+ # TODO messages will be a ValidationMessages class rather than a free-rein dictionary
30
+ def process_messages(self, messages: dict):
31
+ if not messages:
32
+ return
33
+
34
+ if messages.get("errors"):
35
+ self.error("\n".join(messages["errors"]))
36
+
37
+ if messages.get("warnings"):
38
+ self.warn("\n".join(messages["warnings"]))
39
+
40
+ if messages.get("info"):
41
+ self.info("\n".join(messages["info"]))
42
+
29
43
 
30
44
  class ClickIOProviderException(PlatformException):
31
45
  pass
@@ -1,65 +1,269 @@
1
- import boto3
1
+ from boto3 import Session
2
2
 
3
3
  from dbt_platform_helper.platform_exception import PlatformException
4
+ from dbt_platform_helper.providers.io import ClickIOProvider
5
+ from dbt_platform_helper.utils.aws import get_aws_session_or_abort
4
6
 
5
- # TODO - a good candidate for a dataclass when this is refactored into a class.
6
- # Below methods should also really be refactored to not be so tightly coupled with eachother.
7
7
 
8
+ def normalise_to_cidr(ip: str):
9
+ if "/" in ip:
10
+ return ip
11
+ SINGLE_IPV4_CIDR_PREFIX_LENGTH = "32"
12
+ return f"{ip}/{SINGLE_IPV4_CIDR_PREFIX_LENGTH}"
8
13
 
9
- def get_load_balancer_for_application(session: boto3.Session, app: str, env: str) -> str:
10
- lb_client = session.client("elbv2")
11
14
 
12
- describe_response = lb_client.describe_load_balancers()
13
- load_balancers = [lb["LoadBalancerArn"] for lb in describe_response["LoadBalancers"]]
15
+ class LoadBalancerProvider:
14
16
 
15
- load_balancers = lb_client.describe_tags(ResourceArns=load_balancers)["TagDescriptions"]
17
+ def __init__(self, session: Session = None, io: ClickIOProvider = ClickIOProvider()):
18
+ self.session = session
19
+ self.evlb_client = self._get_client("elbv2")
20
+ self.rg_tagging_client = self._get_client("resourcegroupstaggingapi")
21
+ self.io = io
16
22
 
17
- load_balancer_arn = None
18
- for lb in load_balancers:
19
- tags = {t["Key"]: t["Value"] for t in lb["Tags"]}
20
- if tags.get("copilot-application") == app and tags.get("copilot-environment") == env:
21
- load_balancer_arn = lb["ResourceArn"]
23
+ def _get_client(self, client: str):
24
+ if not self.session:
25
+ self.session = get_aws_session_or_abort()
26
+ return self.session.client(client)
22
27
 
23
- if not load_balancer_arn:
24
- raise LoadBalancerNotFoundException(
25
- f"No load balancer found for {app} in the {env} environment"
26
- )
28
+ def find_target_group(self, app: str, env: str, svc: str) -> str:
29
+ target_group_arn = None
30
+
31
+ response = self.rg_tagging_client.get_resources(
32
+ TagFilters=[
33
+ {
34
+ "Key": "copilot-application",
35
+ "Values": [
36
+ app,
37
+ ],
38
+ "Key": "copilot-environment",
39
+ "Values": [
40
+ env,
41
+ ],
42
+ "Key": "copilot-service",
43
+ "Values": [
44
+ svc,
45
+ ],
46
+ },
47
+ ],
48
+ ResourceTypeFilters=[
49
+ "elasticloadbalancing:targetgroup",
50
+ ],
51
+ ) # TODO should be paginated
52
+ for resource in response["ResourceTagMappingList"]:
53
+ tags = {tag["Key"]: tag["Value"] for tag in resource["Tags"]}
54
+
55
+ if (
56
+ "copilot-service" in tags
57
+ and tags["copilot-service"] == svc
58
+ and "copilot-environment" in tags
59
+ and tags["copilot-environment"] == env
60
+ and "copilot-application" in tags
61
+ and tags["copilot-application"] == app
62
+ ):
63
+ target_group_arn = resource["ResourceARN"]
64
+
65
+ if not target_group_arn:
66
+ self.io.error(
67
+ f"No target group found for application: {app}, environment: {env}, service: {svc}",
68
+ )
69
+
70
+ return target_group_arn
71
+
72
+ def get_https_certificate_for_application(self, app: str, env: str) -> str:
73
+ listener_arn = self.get_https_listener_for_application(app, env)
74
+ certificates = self.evlb_client.describe_listener_certificates(ListenerArn=listener_arn)[
75
+ "Certificates"
76
+ ] # TODO should be paginated
77
+
78
+ try:
79
+ certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
80
+ except StopIteration:
81
+ raise CertificateNotFoundException(env)
82
+
83
+ return certificate_arn
84
+
85
+ def get_https_listener_for_application(self, app: str, env: str) -> str:
86
+ load_balancer_arn = self.get_load_balancer_for_application(app, env)
87
+
88
+ listeners = self.evlb_client.describe_listeners(LoadBalancerArn=load_balancer_arn)[
89
+ "Listeners"
90
+ ] # TODO should be paginated
91
+
92
+ listener_arn = None
27
93
 
28
- return load_balancer_arn
94
+ try:
95
+ listener_arn = next(l["ListenerArn"] for l in listeners if l["Protocol"] == "HTTPS")
96
+ except StopIteration:
97
+ pass
29
98
 
99
+ if not listener_arn:
100
+ raise ListenerNotFoundException(app, env)
30
101
 
31
- def get_https_listener_for_application(session: boto3.Session, app: str, env: str) -> str:
32
- load_balancer_arn = get_load_balancer_for_application(session, app, env)
33
- lb_client = session.client("elbv2")
34
- listeners = lb_client.describe_listeners(LoadBalancerArn=load_balancer_arn)["Listeners"]
102
+ return listener_arn
35
103
 
36
- listener_arn = None
104
+ def get_load_balancer_for_application(self, app: str, env: str) -> str:
105
+ describe_response = self.evlb_client.describe_load_balancers()
106
+ load_balancers = [lb["LoadBalancerArn"] for lb in describe_response["LoadBalancers"]]
37
107
 
38
- try:
39
- listener_arn = next(l["ListenerArn"] for l in listeners if l["Protocol"] == "HTTPS")
40
- except StopIteration:
41
- pass
108
+ tag_descriptions = []
109
+ for i in range(0, len(load_balancers), 20):
110
+ chunk = load_balancers[i : i + 20]
111
+ tag_descriptions.extend(
112
+ self.evlb_client.describe_tags(ResourceArns=chunk)["TagDescriptions"]
113
+ )
42
114
 
43
- if not listener_arn:
44
- raise ListenerNotFoundException(f"No HTTPS listener for {app} in the {env} environment")
115
+ for lb in tag_descriptions:
116
+ tags = {t["Key"]: t["Value"] for t in lb["Tags"]}
117
+ # TODO copilot hangover, creates coupling to specific tags could update to check application and environment
118
+ if tags.get("copilot-application") == app and tags.get("copilot-environment") == env:
119
+ return lb["ResourceArn"]
45
120
 
46
- return listener_arn
121
+ raise LoadBalancerNotFoundException(app, env)
47
122
 
123
+ def get_host_header_conditions(self, listener_arn: str, target_group_arn: str) -> list:
124
+ rules = self.evlb_client.describe_rules(ListenerArn=listener_arn)[
125
+ "Rules"
126
+ ] # TODO should be paginated
48
127
 
49
- def get_https_certificate_for_application(session: boto3.Session, app: str, env: str) -> str:
128
+ conditions = []
50
129
 
51
- listener_arn = get_https_listener_for_application(session, app, env)
52
- cert_client = session.client("elbv2")
53
- certificates = cert_client.describe_listener_certificates(ListenerArn=listener_arn)[
54
- "Certificates"
55
- ]
130
+ for rule in rules:
131
+ for action in rule["Actions"]:
132
+ if action["Type"] == "forward" and action["TargetGroupArn"] == target_group_arn:
133
+ conditions = rule["Conditions"]
56
134
 
57
- try:
58
- certificate_arn = next(c["CertificateArn"] for c in certificates if c["IsDefault"])
59
- except StopIteration:
60
- raise CertificateNotFoundException(env)
135
+ if not conditions:
136
+ raise ListenerRuleConditionsNotFoundException(listener_arn)
137
+
138
+ # filter to host-header conditions
139
+ conditions = [
140
+ {i: condition[i] for i in condition if i != "Values"}
141
+ for condition in conditions
142
+ if condition["Field"] == "host-header"
143
+ ]
144
+
145
+ # remove internal hosts
146
+ conditions[0]["HostHeaderConfig"]["Values"] = [
147
+ v for v in conditions[0]["HostHeaderConfig"]["Values"]
148
+ ]
149
+
150
+ return conditions
151
+
152
+ def get_rules_tag_descriptions_by_listener_arn(self, listener_arn: str) -> list:
153
+ rules = self.evlb_client.describe_rules(ListenerArn=listener_arn)[
154
+ "Rules"
155
+ ] # TODO should be paginated
156
+ return self.get_rules_tag_descriptions(rules)
157
+
158
+ def get_rules_tag_descriptions(self, rules: list) -> list:
159
+ tag_descriptions = []
160
+ chunk_size = 20
161
+
162
+ for i in range(0, len(rules), chunk_size):
163
+ chunk = rules[i : i + chunk_size]
164
+ resource_arns = [r["RuleArn"] for r in chunk]
165
+ response = self.evlb_client.describe_tags(
166
+ ResourceArns=resource_arns
167
+ ) # TODO should be paginated
168
+ tag_descriptions.extend(response["TagDescriptions"])
169
+
170
+ return tag_descriptions
171
+
172
+ def create_rule(
173
+ self,
174
+ listener_arn: str,
175
+ actions: list,
176
+ conditions: list,
177
+ priority: int,
178
+ tags: list,
179
+ ):
180
+ return self.evlb_client.create_rule(
181
+ ListenerArn=listener_arn,
182
+ Priority=priority,
183
+ Conditions=conditions,
184
+ Actions=actions,
185
+ Tags=tags,
186
+ )
187
+
188
+ def create_forward_rule(
189
+ self,
190
+ listener_arn: str,
191
+ target_group_arn: str,
192
+ rule_name: str,
193
+ priority: int,
194
+ conditions: list,
195
+ ):
196
+ return self.create_rule(
197
+ listener_arn=listener_arn,
198
+ priority=priority,
199
+ conditions=conditions,
200
+ actions=[{"Type": "forward", "TargetGroupArn": target_group_arn}],
201
+ tags=[
202
+ {"Key": "name", "Value": rule_name},
203
+ ],
204
+ )
61
205
 
62
- return certificate_arn
206
+ def create_header_rule(
207
+ self,
208
+ listener_arn: str,
209
+ target_group_arn: str,
210
+ header_name: str,
211
+ values: list,
212
+ rule_name: str,
213
+ priority: int,
214
+ conditions: list,
215
+ ):
216
+
217
+ combined_conditions = [
218
+ {
219
+ "Field": "http-header",
220
+ "HttpHeaderConfig": {"HttpHeaderName": header_name, "Values": values},
221
+ }
222
+ ] + conditions
223
+
224
+ self.create_forward_rule(
225
+ listener_arn, target_group_arn, rule_name, priority, combined_conditions
226
+ )
227
+
228
+ self.io.info(
229
+ f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\n\nIf request header {header_name} contains one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.",
230
+ )
231
+
232
+ def create_source_ip_rule(
233
+ self,
234
+ listener_arn: str,
235
+ target_group_arn: str,
236
+ values: list,
237
+ rule_name: str,
238
+ priority: int,
239
+ conditions: list,
240
+ ):
241
+ combined_conditions = [
242
+ {
243
+ "Field": "source-ip",
244
+ "SourceIpConfig": {"Values": [normalise_to_cidr(value) for value in values]},
245
+ }
246
+ ] + conditions
247
+
248
+ self.create_forward_rule(
249
+ listener_arn, target_group_arn, rule_name, priority, combined_conditions
250
+ )
251
+
252
+ self.io.info(
253
+ f"Creating listener rule {rule_name} for HTTPS Listener with arn {listener_arn}.\n\nIf request source ip matches one of the values {values}, the request will be forwarded to target group with arn {target_group_arn}.",
254
+ )
255
+
256
+ def delete_listener_rule_by_tags(self, tag_descriptions: list, tag_name: str) -> str:
257
+ current_rule_arn = None
258
+
259
+ for description in tag_descriptions:
260
+ tags = {t["Key"]: t["Value"] for t in description["Tags"]}
261
+ if tags.get("name") == tag_name:
262
+ current_rule_arn = description["ResourceArn"]
263
+ if current_rule_arn:
264
+ self.evlb_client.delete_rule(RuleArn=current_rule_arn)
265
+
266
+ return current_rule_arn
63
267
 
64
268
 
65
269
  class LoadBalancerException(PlatformException):
@@ -67,17 +271,28 @@ class LoadBalancerException(PlatformException):
67
271
 
68
272
 
69
273
  class LoadBalancerNotFoundException(LoadBalancerException):
70
- pass
274
+ def __init__(self, application_name, env):
275
+ super().__init__(
276
+ f"No load balancer found for environment {env} in the application {application_name}."
277
+ )
71
278
 
72
279
 
73
280
  class ListenerNotFoundException(LoadBalancerException):
74
- pass
281
+ def __init__(self, application_name, env):
282
+ super().__init__(
283
+ f"No HTTPS listener found for environment {env} in the application {application_name}."
284
+ )
75
285
 
76
286
 
77
287
  class ListenerRuleNotFoundException(LoadBalancerException):
78
288
  pass
79
289
 
80
290
 
291
+ class ListenerRuleConditionsNotFoundException(LoadBalancerException):
292
+ def __init__(self, listener_arn):
293
+ super().__init__(f"No listener rule conditions found for listener ARN: {listener_arn}")
294
+
295
+
81
296
  class CertificateNotFoundException(PlatformException):
82
297
  def __init__(self, environment_name: str):
83
298
  super().__init__(
@@ -0,0 +1,107 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+ from dbt_platform_helper.constants import PLATFORM_HELPER_VERSION_FILE
5
+ from dbt_platform_helper.providers.config import ConfigProvider
6
+ from dbt_platform_helper.providers.files import FileProvider
7
+ from dbt_platform_helper.providers.io import ClickIOProvider
8
+ from dbt_platform_helper.providers.semantic_version import (
9
+ IncompatibleMajorVersionException,
10
+ )
11
+ from dbt_platform_helper.providers.semantic_version import (
12
+ IncompatibleMinorVersionException,
13
+ )
14
+ from dbt_platform_helper.providers.semantic_version import PlatformHelperVersionStatus
15
+ from dbt_platform_helper.providers.semantic_version import SemanticVersion
16
+ from dbt_platform_helper.providers.version import LocalVersionProvider
17
+ from dbt_platform_helper.providers.version import LocalVersionProviderException
18
+ from dbt_platform_helper.providers.version import PyPiVersionProvider
19
+ from dbt_platform_helper.providers.yaml_file import FileProviderException
20
+ from dbt_platform_helper.providers.yaml_file import YamlFileProvider
21
+ from dbt_platform_helper.utils.files import running_as_installed_package
22
+
23
+
24
+ class PlatformHelperVersioning:
25
+ def __init__(
26
+ self,
27
+ io: ClickIOProvider = ClickIOProvider(),
28
+ file_provider: FileProvider = YamlFileProvider,
29
+ config_provider: ConfigProvider = ConfigProvider(),
30
+ pypi_provider: PyPiVersionProvider = PyPiVersionProvider,
31
+ local_version_provider: LocalVersionProvider = LocalVersionProvider(),
32
+ ):
33
+ self.io = io
34
+ self.file_provider = file_provider
35
+ self.config_provider = config_provider
36
+ self.pypi_provider = pypi_provider
37
+ self.local_version_provider = local_version_provider
38
+
39
+ def get_status(
40
+ self,
41
+ include_project_versions: bool = True,
42
+ ) -> PlatformHelperVersionStatus:
43
+ try:
44
+ locally_installed_version = self.local_version_provider.get_installed_tool_version(
45
+ "dbt-platform-helper"
46
+ )
47
+ except LocalVersionProviderException:
48
+ locally_installed_version = None
49
+
50
+ latest_release = self.pypi_provider.get_latest_version("dbt-platform-helper")
51
+
52
+ if not include_project_versions:
53
+ return PlatformHelperVersionStatus(
54
+ local=locally_installed_version,
55
+ latest=latest_release,
56
+ )
57
+
58
+ deprecated_version_file = Path(PLATFORM_HELPER_VERSION_FILE)
59
+ try:
60
+ loaded_version = self.file_provider.load(deprecated_version_file)
61
+ version_from_file = SemanticVersion.from_string(loaded_version)
62
+ except FileProviderException:
63
+ version_from_file = None
64
+
65
+ platform_config_default, pipeline_overrides = None, {}
66
+
67
+ platform_config = self.config_provider.load_unvalidated_config_file()
68
+
69
+ if platform_config:
70
+ platform_config_default = SemanticVersion.from_string(
71
+ platform_config.get("default_versions", {}).get("platform-helper")
72
+ )
73
+
74
+ pipeline_overrides = {
75
+ name: pipeline.get("versions", {}).get("platform-helper")
76
+ for name, pipeline in platform_config.get("environment_pipelines", {}).items()
77
+ if pipeline.get("versions", {}).get("platform-helper")
78
+ }
79
+
80
+ out = PlatformHelperVersionStatus(
81
+ local=locally_installed_version,
82
+ latest=latest_release,
83
+ deprecated_version_file=version_from_file,
84
+ platform_config_default=platform_config_default,
85
+ pipeline_overrides=pipeline_overrides,
86
+ )
87
+
88
+ return out
89
+
90
+ def check_if_needs_update(self):
91
+ if not running_as_installed_package() or "PLATFORM_TOOLS_SKIP_VERSION_CHECK" in os.environ:
92
+ return
93
+
94
+ version_status = self.get_status(include_project_versions=False)
95
+
96
+ message = (
97
+ f"You are running platform-helper v{version_status.local}, upgrade to "
98
+ f"v{version_status.latest} by running run `pip install "
99
+ "--upgrade dbt-platform-helper`."
100
+ )
101
+
102
+ try:
103
+ version_status.local.validate_compatibility_with(version_status.latest)
104
+ except IncompatibleMajorVersionException:
105
+ self.io.error(message)
106
+ except IncompatibleMinorVersionException:
107
+ self.io.warn(message)
@@ -74,17 +74,22 @@ class SemanticVersion:
74
74
  return SemanticVersion(output_version[0], output_version[1], output_version[2])
75
75
 
76
76
 
77
+ @dataclass
77
78
  class VersionStatus:
78
- def __init__(
79
- self, local_version: SemanticVersion = None, latest_release: SemanticVersion = None
80
- ):
81
- self.local = local_version
82
- self.latest = latest_release
79
+ local: SemanticVersion = None
80
+ latest: SemanticVersion = None
81
+
82
+ def __str__(self):
83
+ attrs = {
84
+ key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
85
+ }
86
+ attrs_str = ", ".join(f"{key}: {value}" for key, value in attrs.items())
87
+ return f"{self.__class__.__name__}: {attrs_str}"
83
88
 
84
89
  def is_outdated(self):
85
90
  return self.local != self.latest
86
91
 
87
- def warn(self):
92
+ def validate(self):
88
93
  pass
89
94
 
90
95
 
@@ -96,7 +101,22 @@ class PlatformHelperVersionStatus(VersionStatus):
96
101
  platform_config_default: Optional[SemanticVersion] = None
97
102
  pipeline_overrides: Optional[Dict[str, str]] = field(default_factory=dict)
98
103
 
99
- def warn(self) -> dict:
104
+ def __str__(self):
105
+ semantic_version_attrs = {
106
+ key: value for key, value in vars(self).items() if isinstance(value, SemanticVersion)
107
+ }
108
+
109
+ class_str = ", ".join(f"{key}: {value}" for key, value in semantic_version_attrs.items())
110
+
111
+ if self.pipeline_overrides.items():
112
+ pipeline_overrides_str = "pipeline_overrides: " + ", ".join(
113
+ f"{key}: {value}" for key, value in self.pipeline_overrides.items()
114
+ )
115
+ class_str = ", ".join([class_str, pipeline_overrides_str])
116
+
117
+ return f"{self.__class__.__name__}: {class_str}"
118
+
119
+ def validate(self) -> dict:
100
120
  if self.platform_config_default and not self.deprecated_version_file:
101
121
  return {}
102
122
 
@@ -1,14 +1,38 @@
1
1
  from abc import ABC
2
+ from importlib.metadata import PackageNotFoundError
3
+ from importlib.metadata import version
2
4
 
3
5
  import requests
4
6
 
7
+ from dbt_platform_helper.platform_exception import PlatformException
5
8
  from dbt_platform_helper.providers.semantic_version import SemanticVersion
6
9
 
7
10
 
11
+ class LocalVersionProviderException(PlatformException):
12
+ pass
13
+
14
+
15
+ class InstalledToolNotFoundException(LocalVersionProviderException):
16
+ def __init__(
17
+ self,
18
+ tool_name: str,
19
+ ):
20
+ super().__init__(f"Package '{tool_name}' not found.")
21
+
22
+
8
23
  class VersionProvider(ABC):
9
24
  pass
10
25
 
11
26
 
27
+ class LocalVersionProvider:
28
+ @staticmethod
29
+ def get_installed_tool_version(tool_name: str) -> SemanticVersion:
30
+ try:
31
+ return SemanticVersion.from_string(version(tool_name))
32
+ except PackageNotFoundError:
33
+ raise InstalledToolNotFoundException(tool_name)
34
+
35
+
12
36
  # TODO add timeouts and exception handling for requests
13
37
  # TODO Alternatively use the gitpython package?
14
38
  class GithubVersionProvider(VersionProvider):
@@ -146,3 +146,17 @@ class ApplicationNotFoundException(ApplicationException):
146
146
  super().__init__(
147
147
  f"""The account "{os.environ.get("AWS_PROFILE")}" does not contain the application "{application_name}"; ensure you have set the environment variable "AWS_PROFILE" correctly."""
148
148
  )
149
+
150
+
151
+ class ApplicationServiceNotFoundException(ApplicationException):
152
+ def __init__(self, application_name: str, svc_name: str):
153
+ super().__init__(
154
+ f"""The service {svc_name} was not found in the application {application_name}. It either does not exist, or has not been deployed."""
155
+ )
156
+
157
+
158
+ class ApplicationEnvironmentNotFoundException(ApplicationException):
159
+ def __init__(self, application_name: str, environment: str):
160
+ super().__init__(
161
+ f"""The environment "{environment}" either does not exist or has not been deployed for the application {application_name}."""
162
+ )
@@ -51,3 +51,9 @@ def generate_override_files_from_template(base_path, overrides_path, output_dir,
51
51
 
52
52
  generate_files_for_dir("*")
53
53
  generate_files_for_dir("bin/*")
54
+
55
+
56
+ # TODO: we've moved this from versioning utils and removed the duplication in platform_helper_versioning
57
+ # Need to review if this is the correct place for this function to reside longer-term
58
+ def running_as_installed_package():
59
+ return "site-packages" in __file__