dbt-platform-helper 13.1.0__py3-none-any.whl → 15.16.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 (95) hide show
  1. dbt_platform_helper/COMMANDS.md +107 -27
  2. dbt_platform_helper/commands/application.py +5 -6
  3. dbt_platform_helper/commands/codebase.py +31 -10
  4. dbt_platform_helper/commands/conduit.py +3 -5
  5. dbt_platform_helper/commands/config.py +20 -311
  6. dbt_platform_helper/commands/copilot.py +18 -391
  7. dbt_platform_helper/commands/database.py +17 -9
  8. dbt_platform_helper/commands/environment.py +20 -14
  9. dbt_platform_helper/commands/generate.py +0 -3
  10. dbt_platform_helper/commands/internal.py +140 -0
  11. dbt_platform_helper/commands/notify.py +58 -78
  12. dbt_platform_helper/commands/pipeline.py +23 -19
  13. dbt_platform_helper/commands/secrets.py +39 -93
  14. dbt_platform_helper/commands/version.py +7 -12
  15. dbt_platform_helper/constants.py +52 -7
  16. dbt_platform_helper/domain/codebase.py +89 -39
  17. dbt_platform_helper/domain/conduit.py +335 -76
  18. dbt_platform_helper/domain/config.py +381 -0
  19. dbt_platform_helper/domain/copilot.py +398 -0
  20. dbt_platform_helper/domain/copilot_environment.py +8 -8
  21. dbt_platform_helper/domain/database_copy.py +2 -2
  22. dbt_platform_helper/domain/maintenance_page.py +254 -430
  23. dbt_platform_helper/domain/notify.py +64 -0
  24. dbt_platform_helper/domain/pipelines.py +43 -35
  25. dbt_platform_helper/domain/plans.py +41 -0
  26. dbt_platform_helper/domain/secrets.py +279 -0
  27. dbt_platform_helper/domain/service.py +570 -0
  28. dbt_platform_helper/domain/terraform_environment.py +14 -13
  29. dbt_platform_helper/domain/update_alb_rules.py +412 -0
  30. dbt_platform_helper/domain/versioning.py +249 -0
  31. dbt_platform_helper/{providers → entities}/platform_config_schema.py +75 -82
  32. dbt_platform_helper/entities/semantic_version.py +83 -0
  33. dbt_platform_helper/entities/service.py +339 -0
  34. dbt_platform_helper/platform_exception.py +4 -0
  35. dbt_platform_helper/providers/autoscaling.py +24 -0
  36. dbt_platform_helper/providers/aws/__init__.py +0 -0
  37. dbt_platform_helper/providers/aws/exceptions.py +70 -0
  38. dbt_platform_helper/providers/aws/interfaces.py +13 -0
  39. dbt_platform_helper/providers/aws/opensearch.py +23 -0
  40. dbt_platform_helper/providers/aws/redis.py +21 -0
  41. dbt_platform_helper/providers/aws/sso_auth.py +75 -0
  42. dbt_platform_helper/providers/cache.py +40 -4
  43. dbt_platform_helper/providers/cloudformation.py +1 -1
  44. dbt_platform_helper/providers/config.py +137 -19
  45. dbt_platform_helper/providers/config_validator.py +112 -51
  46. dbt_platform_helper/providers/copilot.py +24 -16
  47. dbt_platform_helper/providers/ecr.py +89 -7
  48. dbt_platform_helper/providers/ecs.py +228 -36
  49. dbt_platform_helper/providers/environment_variable.py +24 -0
  50. dbt_platform_helper/providers/files.py +1 -1
  51. dbt_platform_helper/providers/io.py +36 -4
  52. dbt_platform_helper/providers/kms.py +22 -0
  53. dbt_platform_helper/providers/load_balancers.py +402 -42
  54. dbt_platform_helper/providers/logs.py +72 -0
  55. dbt_platform_helper/providers/parameter_store.py +134 -0
  56. dbt_platform_helper/providers/s3.py +21 -0
  57. dbt_platform_helper/providers/schema_migrations/__init__.py +0 -0
  58. dbt_platform_helper/providers/schema_migrations/schema_v0_to_v1_migration.py +43 -0
  59. dbt_platform_helper/providers/schema_migrator.py +77 -0
  60. dbt_platform_helper/providers/secrets.py +5 -5
  61. dbt_platform_helper/providers/slack_channel_notifier.py +62 -0
  62. dbt_platform_helper/providers/terraform_manifest.py +121 -19
  63. dbt_platform_helper/providers/version.py +106 -23
  64. dbt_platform_helper/providers/version_status.py +27 -0
  65. dbt_platform_helper/providers/vpc.py +36 -5
  66. dbt_platform_helper/providers/yaml_file.py +58 -2
  67. dbt_platform_helper/templates/environment-pipelines/main.tf +4 -3
  68. dbt_platform_helper/templates/svc/overrides/cfn.patches.yml +5 -0
  69. dbt_platform_helper/utilities/decorators.py +103 -0
  70. dbt_platform_helper/utils/application.py +119 -22
  71. dbt_platform_helper/utils/aws.py +39 -150
  72. dbt_platform_helper/utils/deep_merge.py +10 -0
  73. dbt_platform_helper/utils/git.py +1 -14
  74. dbt_platform_helper/utils/validation.py +1 -1
  75. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/METADATA +11 -20
  76. dbt_platform_helper-15.16.0.dist-info/RECORD +118 -0
  77. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/WHEEL +1 -1
  78. platform_helper.py +3 -1
  79. terraform/elasticache-redis/plans.yml +85 -0
  80. terraform/opensearch/plans.yml +71 -0
  81. terraform/postgres/plans.yml +128 -0
  82. dbt_platform_helper/addon-plans.yml +0 -224
  83. dbt_platform_helper/providers/aws.py +0 -37
  84. dbt_platform_helper/providers/opensearch.py +0 -36
  85. dbt_platform_helper/providers/redis.py +0 -34
  86. dbt_platform_helper/providers/semantic_version.py +0 -126
  87. dbt_platform_helper/templates/svc/manifest-backend.yml +0 -69
  88. dbt_platform_helper/templates/svc/manifest-public.yml +0 -109
  89. dbt_platform_helper/utils/cloudfoundry.py +0 -14
  90. dbt_platform_helper/utils/files.py +0 -53
  91. dbt_platform_helper/utils/manifests.py +0 -18
  92. dbt_platform_helper/utils/versioning.py +0 -238
  93. dbt_platform_helper-13.1.0.dist-info/RECORD +0 -96
  94. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info}/entry_points.txt +0 -0
  95. {dbt_platform_helper-13.1.0.dist-info → dbt_platform_helper-15.16.0.dist-info/licenses}/LICENSE +0 -0
@@ -8,15 +8,22 @@ from schema import Regex
8
8
  from schema import Schema
9
9
  from schema import SchemaError
10
10
 
11
+ from dbt_platform_helper.constants import PLATFORM_CONFIG_SCHEMA_VERSION
12
+ from dbt_platform_helper.domain.plans import PlanLoader
13
+
14
+ plan_manager = PlanLoader()
15
+ plan_manager.load()
16
+
11
17
 
12
18
  class PlatformConfigSchema:
13
19
  @staticmethod
14
20
  def schema() -> Schema:
15
21
  return Schema(
16
22
  {
23
+ "schema_version": PLATFORM_CONFIG_SCHEMA_VERSION,
17
24
  "application": str,
18
25
  Optional("deploy_repository"): str,
19
- Optional("default_versions"): PlatformConfigSchema.__default_versions_schema(),
26
+ "default_versions": PlatformConfigSchema.__default_versions_schema(),
20
27
  Optional("environments"): PlatformConfigSchema.__environments_schema(),
21
28
  Optional("codebase_pipelines"): PlatformConfigSchema.__codebase_pipelines_schema(),
22
29
  Optional(
@@ -28,6 +35,7 @@ class PlatformConfigSchema:
28
35
  PlatformConfigSchema.__monitoring_schema(),
29
36
  PlatformConfigSchema.__opensearch_schema(),
30
37
  PlatformConfigSchema.__postgres_schema(),
38
+ PlatformConfigSchema.__datadog_schema(),
31
39
  PlatformConfigSchema.__prometheus_policy_schema(),
32
40
  PlatformConfigSchema.__redis_schema(),
33
41
  PlatformConfigSchema.__s3_bucket_schema(),
@@ -48,12 +56,13 @@ class PlatformConfigSchema:
48
56
  "postgres": Schema(PlatformConfigSchema.__postgres_schema()),
49
57
  "prometheus-policy": Schema(PlatformConfigSchema.__prometheus_policy_schema()),
50
58
  "redis": Schema(PlatformConfigSchema.__redis_schema()),
59
+ "datadog": Schema(PlatformConfigSchema.__datadog_schema()),
51
60
  "s3": Schema(PlatformConfigSchema.__s3_bucket_schema()),
52
61
  "s3-policy": Schema(PlatformConfigSchema.__s3_bucket_policy_schema()),
53
62
  "subscription-filter": PlatformConfigSchema.__no_configuration_required_schema(
54
63
  "subscription-filter"
55
64
  ),
56
- # Todo: The next three are no longer relevant. Remove them.
65
+ # TODO: DBTP-1943: The next three are no longer relevant. Remove them.
57
66
  "monitoring": Schema(PlatformConfigSchema.__monitoring_schema()),
58
67
  "vpc": PlatformConfigSchema.__no_configuration_required_schema("vpc"),
59
68
  "xray": PlatformConfigSchema.__no_configuration_required_schema("xray"),
@@ -105,7 +114,6 @@ class PlatformConfigSchema:
105
114
  Optional("default_waf"): str,
106
115
  Optional("domain_prefix"): str,
107
116
  Optional("enable_logging"): bool,
108
- Optional("env_root"): str,
109
117
  Optional("forwarded_values_forward"): str,
110
118
  Optional("forwarded_values_headers"): [str],
111
119
  Optional("forwarded_values_query_string"): bool,
@@ -143,7 +151,7 @@ class PlatformConfigSchema:
143
151
  {
144
152
  "name": str,
145
153
  Optional("requires_approval"): bool,
146
- }
154
+ },
147
155
  ],
148
156
  },
149
157
  {
@@ -158,22 +166,24 @@ class PlatformConfigSchema:
158
166
  },
159
167
  ),
160
168
  ],
169
+ Optional("cache_invalidation"): {
170
+ "domains": PlatformConfigSchema.__cache_invalidation_domains_schema(),
171
+ },
161
172
  },
162
173
  }
163
174
 
175
+ @staticmethod
176
+ def __cache_invalidation_domains_schema() -> dict:
177
+ return {str: {"paths": [str], "environment": str}}
178
+
164
179
  @staticmethod
165
180
  def __default_versions_schema() -> dict:
166
181
  return {
167
- Optional("terraform-platform-modules"): str,
168
- Optional("platform-helper"): str,
182
+ "platform-helper": str,
169
183
  }
170
184
 
171
185
  @staticmethod
172
186
  def __environments_schema() -> dict:
173
- _valid_environment_specific_version_overrides = {
174
- Optional("terraform-platform-modules"): str,
175
- }
176
-
177
187
  return {
178
188
  str: Or(
179
189
  None,
@@ -188,10 +198,15 @@ class PlatformConfigSchema:
188
198
  "id": str,
189
199
  },
190
200
  },
191
- # Todo: requires_approval is no longer relevant since we don't have AWS Copilot manage environment pipelines
201
+ # TODO: DBTP-1943: requires_approval is no longer relevant since we don't have AWS Copilot manage environment pipelines
192
202
  Optional("requires_approval"): bool,
193
- Optional("versions"): _valid_environment_specific_version_overrides,
194
203
  Optional("vpc"): str,
204
+ Optional("service-deployment-mode"): Or(
205
+ "copilot",
206
+ "dual-deploy-copilot-traffic",
207
+ "dual-deploy-platform-traffic",
208
+ "platform",
209
+ ),
195
210
  },
196
211
  )
197
212
  }
@@ -248,18 +263,8 @@ class PlatformConfigSchema:
248
263
 
249
264
  @staticmethod
250
265
  def __opensearch_schema() -> dict:
251
- # Todo: Move to OpenSearch provider?
252
- _valid_opensearch_plans = Or(
253
- "tiny",
254
- "small",
255
- "small-ha",
256
- "medium",
257
- "medium-ha",
258
- "large",
259
- "large-ha",
260
- "x-large",
261
- "x-large-ha",
262
- )
266
+ # TODO: DBTP-1943: Move to OpenSearch provider?
267
+ _valid_opensearch_plans = Or(*plan_manager.get_plan_names("opensearch"))
263
268
 
264
269
  return {
265
270
  "type": "opensearch",
@@ -286,47 +291,27 @@ class PlatformConfigSchema:
286
291
 
287
292
  @staticmethod
288
293
  def __postgres_schema() -> dict:
289
- # Todo: Move to Postgres provider?
290
- _valid_postgres_plans = Or(
291
- "tiny",
292
- "small",
293
- "small-ha",
294
- "small-high-io",
295
- "medium",
296
- "medium-ha",
297
- "medium-high-io",
298
- "large",
299
- "large-ha",
300
- "large-high-io",
301
- "x-large",
302
- "x-large-ha",
303
- "x-large-high-io",
304
- "2x-large",
305
- "2x-large-ha",
306
- "2x-large-high-io",
307
- "4x-large",
308
- "4x-large-ha",
309
- "4x-large-high-io",
310
- )
294
+ _valid_postgres_plans = Or(*plan_manager.get_plan_names("postgres"))
295
+ _valid_postgres_version = Or(int, float)
311
296
 
312
- # Todo: Move to Postgres provider?
297
+ # TODO: DBTP-1943: Move to Postgres provider?
313
298
  _valid_postgres_storage_types = Or("gp2", "gp3", "io1", "io2")
314
299
 
315
300
  _valid_postgres_database_copy = {
316
301
  "from": PlatformConfigSchema.__valid_environment_name(),
317
302
  "to": PlatformConfigSchema.__valid_environment_name(),
318
- Optional("from_account"): str,
319
- Optional("to_account"): str,
320
303
  Optional("pipeline"): {Optional("schedule"): str},
321
304
  }
322
305
 
323
306
  return {
324
307
  "type": "postgres",
325
- "version": (Or(int, float)),
308
+ Optional("version"): _valid_postgres_version,
326
309
  Optional("deletion_policy"): PlatformConfigSchema.__valid_postgres_deletion_policy(),
327
310
  Optional("environments"): {
328
311
  PlatformConfigSchema.__valid_environment_name(): {
312
+ Optional("apply_immediately"): bool,
329
313
  Optional("plan"): _valid_postgres_plans,
314
+ Optional("version"): (Or(int, float)),
330
315
  Optional("volume_size"): PlatformConfigSchema.is_integer_between(20, 10000),
331
316
  Optional("iops"): PlatformConfigSchema.is_integer_between(1000, 9950),
332
317
  Optional("snapshot_id"): str,
@@ -336,9 +321,6 @@ class PlatformConfigSchema:
336
321
  Optional("deletion_protection"): bool,
337
322
  Optional("multi_az"): bool,
338
323
  Optional("storage_type"): _valid_postgres_storage_types,
339
- Optional("backup_retention_days"): PlatformConfigSchema.is_integer_between(
340
- 1, 35
341
- ),
342
324
  }
343
325
  },
344
326
  Optional("database_copy"): [_valid_postgres_database_copy],
@@ -364,21 +346,7 @@ class PlatformConfigSchema:
364
346
 
365
347
  @staticmethod
366
348
  def __redis_schema() -> dict:
367
- # Todo move to Redis provider?
368
- _valid_redis_plans = Or(
369
- "micro",
370
- "micro-ha",
371
- "tiny",
372
- "tiny-ha",
373
- "small",
374
- "small-ha",
375
- "medium",
376
- "medium-ha",
377
- "large",
378
- "large-ha",
379
- "x-large",
380
- "x-large-ha",
381
- )
349
+ _valid_redis_plans = Or(*plan_manager.get_plan_names("redis"))
382
350
 
383
351
  return {
384
352
  "type": "redis",
@@ -398,7 +366,7 @@ class PlatformConfigSchema:
398
366
 
399
367
  @staticmethod
400
368
  def valid_s3_bucket_name(name: str):
401
- # Todo: This is a public method becasue that's what the test expect. Perhaps it belongs in an S3 provider?
369
+ # TODO: DBTP-1943: This is a public method becasue that's what the test expect. Perhaps it belongs in an S3 provider?
402
370
  errors = []
403
371
  if not (2 < len(name) < 64):
404
372
  errors.append("Length must be between 3 and 63 characters inclusive.")
@@ -427,13 +395,36 @@ class PlatformConfigSchema:
427
395
  errors.append(f"Names cannot be suffixed '{suffix}'.")
428
396
 
429
397
  if errors:
430
- # Todo: Raise suitable PlatformException?
398
+ # TODO: DBTP-1943: Raise suitable PlatformException?
431
399
  raise SchemaError(
432
- "Bucket name '{}' is invalid:\n{}".format(name, "\n".join(f" {e}" for e in errors))
400
+ f"Bucket name '{name}' is invalid:\n" + "\n".join(f" {e}" for e in errors)
433
401
  )
434
402
 
435
403
  return True
436
404
 
405
+ @staticmethod
406
+ def __datadog_schema() -> dict:
407
+ return {
408
+ "type": "datadog",
409
+ Optional("environments"): {
410
+ Optional(PlatformConfigSchema.__valid_environment_name()): {
411
+ "team_name": str,
412
+ Optional("contact_name"): str,
413
+ Optional("contact_email"): str,
414
+ Optional("contacts"): [
415
+ {
416
+ "name": str,
417
+ "type": str,
418
+ "contact": str,
419
+ }
420
+ ],
421
+ Optional("documentation_url"): str,
422
+ "services_to_monitor": dict,
423
+ Optional("description"): str,
424
+ },
425
+ },
426
+ }
427
+
437
428
  @staticmethod
438
429
  def __s3_bucket_schema() -> dict:
439
430
  def _valid_s3_bucket_arn(key):
@@ -506,7 +497,9 @@ class PlatformConfigSchema:
506
497
  },
507
498
  Optional("cross_environment_service_access"): {
508
499
  PlatformConfigSchema.__valid_schema_key(): {
509
- "application": str,
500
+ # Deprecated: We didn't implement cross application access, no service teams are asking for it.
501
+ # application should be removed once we can confirm that no-one is using it.
502
+ Optional("application"): str,
510
503
  "environment": PlatformConfigSchema.__valid_environment_name(),
511
504
  "account": str,
512
505
  "service": str,
@@ -538,10 +531,10 @@ class PlatformConfigSchema:
538
531
 
539
532
  @staticmethod
540
533
  def string_matching_regex(regex_pattern: str) -> Callable:
541
- # Todo public for the unit tests, not sure about testing what could be a private method. Perhaps it's covered by other tests anyway?
534
+ # TODO: DBTP-1943: public for the unit tests, not sure about testing what could be a private method. Perhaps it's covered by other tests anyway?
542
535
  def validate(string):
543
536
  if not re.match(regex_pattern, string):
544
- # Todo: Raise suitable PlatformException?
537
+ # TODO: DBTP-1943: Raise suitable PlatformException?
545
538
  raise SchemaError(
546
539
  f"String '{string}' does not match the required pattern '{regex_pattern}'."
547
540
  )
@@ -551,11 +544,11 @@ class PlatformConfigSchema:
551
544
 
552
545
  @staticmethod
553
546
  def is_integer_between(lower_limit, upper_limit) -> Callable:
554
- # Todo public for the unit tests, not sure about testing what could be a private method. Perhaps it's covered by other tests anyway?
547
+ # TODO: DBTP-1943: public for the unit tests, not sure about testing what could be a private method. Perhaps it's covered by other tests anyway?
555
548
  def validate(value):
556
549
  if isinstance(value, int) and lower_limit <= value <= upper_limit:
557
550
  return True
558
- # Todo: Raise suitable PlatformException?
551
+ # TODO: DBTP-1943: Raise suitable PlatformException?
559
552
  raise SchemaError(f"should be an integer between {lower_limit} and {upper_limit}")
560
553
 
561
554
  return validate
@@ -569,7 +562,7 @@ class PlatformConfigSchema:
569
562
 
570
563
  @staticmethod
571
564
  def __valid_branch_name() -> Callable:
572
- # Todo: Make this actually validate a git branch name properly; https://git-scm.com/docs/git-check-ref-format
565
+ # TODO: DBTP-1943: Make this actually validate a git branch name properly; https://git-scm.com/docs/git-check-ref-format
573
566
  return PlatformConfigSchema.string_matching_regex(r"^((?!\*).)*(\*)?$")
574
567
 
575
568
  @staticmethod
@@ -615,10 +608,10 @@ class PlatformConfigSchema:
615
608
 
616
609
 
617
610
  class ConditionalOpensSearchSchema(Schema):
618
- # Todo: Move to OpenSearch provider?
611
+ # TODO: DBTP-1943: Move to OpenSearch provider?
619
612
  _valid_opensearch_min_volume_size: int = 10
620
613
 
621
- # Todo: Move to OpenSearch provider?
614
+ # TODO: DBTP-1943: Move to OpenSearch provider?
622
615
  _valid_opensearch_max_volume_size: dict = {
623
616
  "tiny": 100,
624
617
  "small": 200,
@@ -652,11 +645,11 @@ class ConditionalOpensSearchSchema(Schema):
652
645
 
653
646
  if volume_size:
654
647
  if not plan:
655
- # Todo: Raise suitable PlatformException?
648
+ # TODO: DBTP-1943: Raise suitable PlatformException?
656
649
  raise SchemaError(f"Missing key: 'plan'")
657
650
 
658
651
  if volume_size < self._valid_opensearch_min_volume_size:
659
- # Todo: Raise suitable PlatformException?
652
+ # TODO: DBTP-1943: Raise suitable PlatformException?
660
653
  raise SchemaError(
661
654
  f"Key 'environments' error: Key '{env}' error: Key 'volume_size' error: should be an integer greater than {self._valid_opensearch_min_volume_size}"
662
655
  )
@@ -666,7 +659,7 @@ class ConditionalOpensSearchSchema(Schema):
666
659
  plan == key
667
660
  and not volume_size <= self._valid_opensearch_max_volume_size[key]
668
661
  ):
669
- # Todo: Raise suitable PlatformException?
662
+ # TODO: DBTP-1943: Raise suitable PlatformException?
670
663
  raise SchemaError(
671
664
  f"Key 'environments' error: Key '{env}' error: Key 'volume_size' error: should be an integer between {self._valid_opensearch_min_volume_size} and {self._valid_opensearch_max_volume_size[key]} for plan {plan}"
672
665
  )
@@ -0,0 +1,83 @@
1
+ import re
2
+ from typing import Union
3
+
4
+ from dbt_platform_helper.providers.validation import ValidationException
5
+
6
+
7
+ class IncompatibleMajorVersionException(ValidationException):
8
+ def __init__(self, app_version: str, check_version: str):
9
+ super().__init__()
10
+ self.app_version = app_version
11
+ self.check_version = check_version
12
+
13
+
14
+ class IncompatibleMinorVersionException(ValidationException):
15
+ def __init__(self, app_version: str, check_version: str):
16
+ super().__init__()
17
+ self.app_version = app_version
18
+ self.check_version = check_version
19
+
20
+
21
+ class SemanticVersion:
22
+ def __init__(self, major: int, minor: int, patch: int):
23
+ self.major = major
24
+ self.minor = minor
25
+ self.patch = patch
26
+
27
+ def __str__(self) -> str:
28
+ if self.major is None:
29
+ return "unknown"
30
+ return ".".join([str(s) for s in [self.major, self.minor, self.patch]])
31
+
32
+ def __repr__(self) -> str:
33
+ return str(self)
34
+
35
+ def __lt__(self, other) -> bool:
36
+ return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
37
+
38
+ def __eq__(self, other) -> bool:
39
+ if other is None:
40
+ return False
41
+ return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
42
+
43
+ def validate_compatibility_with(self, other):
44
+ if other is None:
45
+ raise ValidationException("Cannot compare NoneType")
46
+ if (self.major == 0 and other.major == 0) and (
47
+ self.minor != other.minor or self.patch != other.patch
48
+ ):
49
+ raise IncompatibleMajorVersionException(str(self), str(other))
50
+
51
+ if self.major != other.major:
52
+ raise IncompatibleMajorVersionException(str(self), str(other))
53
+
54
+ if self.minor != other.minor:
55
+ raise IncompatibleMinorVersionException(str(self), str(other))
56
+
57
+ @staticmethod
58
+ def _cast_to_int_with_fallback(input, fallback=-1):
59
+ try:
60
+ return int(input)
61
+ except ValueError:
62
+ return fallback
63
+
64
+ @classmethod
65
+ def from_string(self, version_string: Union[str, None]):
66
+ if version_string is None:
67
+ return None
68
+
69
+ version_segments = re.split(r"[.\-]", version_string.replace("v", ""))
70
+
71
+ if len(version_segments) != 3:
72
+ return None
73
+
74
+ major, minor, patch = [self._cast_to_int_with_fallback(s) for s in version_segments]
75
+
76
+ return SemanticVersion(major, minor, patch)
77
+
78
+ @staticmethod
79
+ def is_semantic_version(version_string):
80
+ if not version_string:
81
+ return False
82
+ valid_semantic_string_regex = r"(?i)^v?[0-9]+[.-][0-9]+[.-][0-9]+$"
83
+ return re.match(valid_semantic_string_regex, version_string)