qontract-reconcile 0.10.1rc440__py3-none-any.whl → 0.10.1rc442__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: qontract-reconcile
3
- Version: 0.10.1rc440
3
+ Version: 0.10.1rc442
4
4
  Summary: Collection of tools to reconcile services with their desired state as defined in the app-interface DB.
5
5
  Home-page: https://github.com/app-sre/qontract-reconcile
6
6
  Author: Red Hat App-SRE Team
@@ -71,7 +71,7 @@ reconcile/openshift_resources.py,sha256=kwsY5cko7udEKNlhL2oKiKv_5wzEw9wmmwROE016
71
71
  reconcile/openshift_resources_base.py,sha256=iKS8mpKnJca6MO5dl1a1az2YdEa_6zDRNf_XS_i9FBA,44936
72
72
  reconcile/openshift_rolebindings.py,sha256=K6alhxtnxifnytQKMqIGdVkqGEa28AVwFv4B7SjbgIk,6628
73
73
  reconcile/openshift_routes.py,sha256=fXvuPSjcjVw1X3j2EQvUAdbOepmIFdKk-M3qP8QzPiw,1075
74
- reconcile/openshift_saas_deploy.py,sha256=QpQAQTeDZPOtgxV9RoAyu2NeX4Jlc4xAslVdgwD0sgQ,10811
74
+ reconcile/openshift_saas_deploy.py,sha256=JkMv21u6JImrNSnKgfseXbg7GrIk9v2zmW1p7WfF9-w,11889
75
75
  reconcile/openshift_saas_deploy_change_tester.py,sha256=spWjxapC-u4TrCAsz1Q6_297QwfrIRx19oqz2bRPQn0,8907
76
76
  reconcile/openshift_saas_deploy_trigger_base.py,sha256=UEKWAJo6cN3Nml89tzJzbnpkJ7efOnFDf9Wfz9_tBdg,14325
77
77
  reconcile/openshift_saas_deploy_trigger_cleaner.py,sha256=tcvziJdw5lgJbbogk0-wKT2aYCFP99sL4qTSfau4otY,2971
@@ -131,7 +131,7 @@ reconcile/aws_ami_cleanup/integration.py,sha256=IW95cpMj2P5ffs-AxsR_TDQCJnYFBhLI
131
131
  reconcile/aws_cloudwatch_log_retention/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
132
132
  reconcile/aws_cloudwatch_log_retention/integration.py,sha256=0UcSZIrGvnGY4m9fj87oejIolIP_qTxtJInpmW9jrQ0,7772
133
133
  reconcile/aws_version_sync/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
- reconcile/aws_version_sync/integration.py,sha256=sddkW_XcHF8KMeqE23Mxl3yLrpUkqzv_ArP-EVXcwIU,11432
134
+ reconcile/aws_version_sync/integration.py,sha256=fCDtfI3Lpz1l5l6AwC9tTnoiMeY7yvFLR6l7twixFlE,12248
135
135
  reconcile/aws_version_sync/utils.py,sha256=sVv-48PKi2VITlqqvmpbjnFDOPeGqfKzgkpIszlmjL0,1708
136
136
  reconcile/aws_version_sync/merge_request_manager/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
137
137
  reconcile/aws_version_sync/merge_request_manager/merge_request.py,sha256=FeNcQaory5AXNVuVk-jJxPwtI4uSoURgkTH3rXAb2cc,6198
@@ -202,7 +202,7 @@ reconcile/gql_definitions/common/ocm_environments.py,sha256=yV4UVjdnNmqbR5trQCOA
202
202
  reconcile/gql_definitions/common/pagerduty_instances.py,sha256=8NBHKRXg_OKG9NsJv6FOj8UVFcjkdJg-9E16ZqZIRPQ,2006
203
203
  reconcile/gql_definitions/common/pgp_reencryption_settings.py,sha256=tS68-tBBd7BJYmfTjtdTlxpABF3f_z9eJdtaKnyZc0Q,2305
204
204
  reconcile/gql_definitions/common/pipeline_providers.py,sha256=5N61ONUkwpytVsrl9icmNfa4M6yR1WXeTQAqvFE7c6c,9421
205
- reconcile/gql_definitions/common/saas_files.py,sha256=tw5iT4xqtM7kRaxYnbaB7bBem8uAr3AiRLEXQ8GAaZw,16089
205
+ reconcile/gql_definitions/common/saas_files.py,sha256=tGLkM7ss_V8fCzEGXaDCm3HgKmuL7_xOxUDwllRukWE,16254
206
206
  reconcile/gql_definitions/common/saas_target_namespaces.py,sha256=ENKqwgZ_jvvmtrN0tWRnUt3k5qgdJBLsPnQPeaZATbs,2768
207
207
  reconcile/gql_definitions/common/saasherder_settings.py,sha256=jxrFr03NmiwV3uegKCxQgB5iveC2IaGZIoguXoiNMgs,1797
208
208
  reconcile/gql_definitions/common/smtp_client_settings.py,sha256=Pb8VgTGFqCh4_rI0BOHoXuicfdNyol1kIN8NLONHaxI,2252
@@ -400,7 +400,7 @@ reconcile/test/test_openshift_namespace_labels.py,sha256=P1hqi6P88NijNrurdXG_QR2
400
400
  reconcile/test/test_openshift_namespaces.py,sha256=HmRnCE5EnFt3MYceVEFHmk8wWRtCrxu2AFGFkY9pdyA,9214
401
401
  reconcile/test/test_openshift_resource.py,sha256=FwU5xzrGyc1FHOL-HA3EH09iktCsdPaGv8HyKNwKit4,13037
402
402
  reconcile/test/test_openshift_resources_base.py,sha256=i4tk9knD5F1DppSsjBJ5pfrLSm1aP7lt--OSx3gmO9M,14058
403
- reconcile/test/test_openshift_saas_deploy.py,sha256=OoS8MNnkNQ6ZBkIgSdfEP3KSiiZeb92HAwXsQ2XnhwM,2713
403
+ reconcile/test/test_openshift_saas_deploy.py,sha256=nbscib0QtIRpuRdgwFdiMpsW5k-ZjkENn40fKCbZg3Q,5891
404
404
  reconcile/test/test_openshift_saas_deploy_change_tester.py,sha256=5naL8Z1vq18CrlZHS_bfuxesUatKc4DXgsVvqBwqvmA,13201
405
405
  reconcile/test/test_openshift_tekton_resources.py,sha256=xkVgV_TGSHTaeTPgbdknWxmYVv8D3xCcXyK7CiMrXW8,11256
406
406
  reconcile/test/test_openshift_upgrade_watcher.py,sha256=na5Q5l88c0ZHMh82gvNpYheoMOAzEZRG0ZvzzDhafYE,7100
@@ -483,7 +483,7 @@ reconcile/typed_queries/namespaces.py,sha256=vItPrn7sfcHOix-VvkzQkf54_ljzI_ymyxh
483
483
  reconcile/typed_queries/namespaces_minimal.py,sha256=rUtqNQ0ORXXUTQfnpsMURymAJ4gYtE77V-Lb3LiJFEY,278
484
484
  reconcile/typed_queries/pagerduty_instances.py,sha256=QCHqEAakiH6eSob0Pnnn3IBd8Ga0zpEp1Z6Qu3v2uH4,733
485
485
  reconcile/typed_queries/repos.py,sha256=RKBsf7IDS6NsXTtXxJ9Ol9G3bxG9sr3vW9QQ2bahEHo,512
486
- reconcile/typed_queries/saas_files.py,sha256=BuDZf83hv6ItJMqBEWBOF14XNyeEwr4qMkEWsA1fTK8,13990
486
+ reconcile/typed_queries/saas_files.py,sha256=tDZFCrTCaSUwB3aCS7AAQkbf1ueZ6koxiaMo-rChFDk,14117
487
487
  reconcile/typed_queries/slo_documents.py,sha256=x2dg0cnMET-ImARzhDg7Q81MA9Zlm9alH8Rp5XkVR6s,407
488
488
  reconcile/typed_queries/smtp.py,sha256=aSLglYa5bHKmlGwKkxq2RZqyMWuAf0a4S_mOuhDa084,542
489
489
  reconcile/typed_queries/status_board.py,sha256=-W_YwyASGCHo5Vs5eqych7sbdoKHznl57Dh4Vp_oOvI,1843
@@ -554,7 +554,7 @@ reconcile/utils/quay_api.py,sha256=EuOegpb-7ntEjkKLFwM2Oo4Nw7SyFtmyl3sQ9aXMtrM,8
554
554
  reconcile/utils/raw_github_api.py,sha256=ZHC-SZuAyRe1zaMoOU7Krt1-zecDxENd9c_NzQYqK9g,2968
555
555
  reconcile/utils/repo_owners.py,sha256=j-pUjc9PuDzq7KpjNLpnhqfU8tUG4nj2WMhFp4ick7g,6629
556
556
  reconcile/utils/secret_reader.py,sha256=2DeYAAQFjUULEKlLw3UDAUoND6gbqvCh9uKPtlc-0us,10403
557
- reconcile/utils/semver_helper.py,sha256=4Rrkz9Cj9A6oHPVgA-nGj6MBoxlFT4eyxTcslWqow3I,771
557
+ reconcile/utils/semver_helper.py,sha256=dp86KxjlOc8LHzawMvbxRfZamv7KU7b2SVnZQL-Xg6U,1142
558
558
  reconcile/utils/sharding.py,sha256=gkYf0lD3IUKQPEmdRJZ70mdDT1c9qWjbdP7evRsUis4,839
559
559
  reconcile/utils/slack_api.py,sha256=fntPGTn4hHRiScEIZu9LDPwbKeNPe9bR2Rmef9c-GIM,15830
560
560
  reconcile/utils/smtp_client.py,sha256=gJNbBQJpAt5PX4t_TaeNHsXM8vt50bFgndml6yK2b5o,2800
@@ -646,8 +646,8 @@ tools/test/test_app_interface_metrics_exporter.py,sha256=dmEcNwZltP1rd_4DbxIYakO
646
646
  tools/test/test_qontract_cli.py,sha256=awwTHEc2DWlykuqGIYM0WOBoSL0KRnOraCLk3C7izis,1401
647
647
  tools/test/test_sd_app_sre_alert_report.py,sha256=v363r9zM7__0kR5K6mvJoGFcM9BvE33fWAayrqkpojA,2116
648
648
  tools/test/test_sre_checkpoints.py,sha256=SKqPPTl9ua0RFdSSofnoQX-JZE6dFLO3LRhfQzqtfh8,2607
649
- qontract_reconcile-0.10.1rc440.dist-info/METADATA,sha256=-uhvfCTgCWPyQyEqyH_cfVF32cjGY01lu5z6kSS0FOM,2348
650
- qontract_reconcile-0.10.1rc440.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
651
- qontract_reconcile-0.10.1rc440.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
652
- qontract_reconcile-0.10.1rc440.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
653
- qontract_reconcile-0.10.1rc440.dist-info/RECORD,,
649
+ qontract_reconcile-0.10.1rc442.dist-info/METADATA,sha256=y-3WObgzBJvlIUo2w7yrbG2GOPcQ6FiJ7nLAJ7-dRPQ,2348
650
+ qontract_reconcile-0.10.1rc442.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
651
+ qontract_reconcile-0.10.1rc442.dist-info/entry_points.txt,sha256=rTjAv28I_CHLM8ID3OPqMI_suoQ9s7tFbim4aYjn9kk,376
652
+ qontract_reconcile-0.10.1rc442.dist-info/top_level.txt,sha256=l5ISPoXzt0SdR4jVdkfa7RPSKNc8zAHYWAnR-Dw8Ey8,24
653
+ qontract_reconcile-0.10.1rc442.dist-info/RECORD,,
@@ -5,7 +5,11 @@ from collections.abc import (
5
5
  )
6
6
  from typing import Any
7
7
 
8
- from pydantic import BaseModel
8
+ import semver
9
+ from pydantic import (
10
+ BaseModel,
11
+ validator,
12
+ )
9
13
 
10
14
  from reconcile.aws_version_sync.merge_request_manager.merge_request import (
11
15
  Parser,
@@ -42,6 +46,7 @@ from reconcile.utils.runtime.integration import (
42
46
  PydanticRunParams,
43
47
  QontractReconcileIntegration,
44
48
  )
49
+ from reconcile.utils.semver_helper import parse_semver
45
50
  from reconcile.utils.unleash import get_feature_toggle_state
46
51
  from reconcile.utils.vcs import VCS
47
52
 
@@ -67,7 +72,10 @@ class ExternalResource(BaseModel):
67
72
  resource_provider: str
68
73
  resource_identifier: str
69
74
  resource_engine: str
70
- resource_engine_version: str
75
+ resource_engine_version: semver.VersionInfo
76
+
77
+ class Config:
78
+ arbitrary_types_allowed = True
71
79
 
72
80
  @property
73
81
  def key(self) -> tuple:
@@ -79,6 +87,14 @@ class ExternalResource(BaseModel):
79
87
  self.resource_engine,
80
88
  )
81
89
 
90
+ @validator("resource_engine_version", pre=True)
91
+ def parse_resource_engine_version( # pylint: disable=no-self-argument
92
+ cls, v: str | semver.VersionInfo
93
+ ) -> semver.VersionInfo:
94
+ if isinstance(v, semver.VersionInfo):
95
+ return v
96
+ return parse_semver(v, optional_minor_and_patch=True)
97
+
82
98
 
83
99
  AwsExternalResources = list[ExternalResource]
84
100
  AppInterfaceExternalResources = list[ExternalResource]
@@ -228,12 +244,18 @@ class AVSIntegration(QontractReconcileIntegration[AVSIntegrationParams]):
228
244
  current=external_resources_app_interface,
229
245
  desired=external_resources_aws,
230
246
  key=lambda r: r.key,
231
- equal=lambda r1, r2: r1.resource_engine_version
232
- == r2.resource_engine_version,
247
+ equal=lambda external_resources_app_interface, external_resources_aws: external_resources_app_interface.resource_engine_version
248
+ == external_resources_aws.resource_engine_version,
233
249
  )
234
250
  for diff_pair in diff.change.values():
235
251
  aws_resource = diff_pair.desired
236
252
  app_interface_resource = diff_pair.current
253
+ if (
254
+ aws_resource.resource_engine_version
255
+ <= app_interface_resource.resource_engine_version
256
+ ):
257
+ # do not downgrade the version
258
+ continue
237
259
  # make mypy happy
238
260
  assert app_interface_resource.namespace_file
239
261
  assert app_interface_resource.provisioner.path
@@ -205,6 +205,7 @@ query SaasFiles {
205
205
  deprecated
206
206
  compare
207
207
  timeout
208
+ skipSuccessfulDeployNotifications
208
209
  publishJobLogs
209
210
  clusterAdmin
210
211
  imagePatterns
@@ -564,6 +565,9 @@ class SaasFileV2(ConfiguredBaseModel):
564
565
  deprecated: Optional[bool] = Field(..., alias="deprecated")
565
566
  compare: Optional[bool] = Field(..., alias="compare")
566
567
  timeout: Optional[str] = Field(..., alias="timeout")
568
+ skip_successful_deploy_notifications: Optional[bool] = Field(
569
+ ..., alias="skipSuccessfulDeployNotifications"
570
+ )
567
571
  publish_job_logs: Optional[bool] = Field(..., alias="publishJobLogs")
568
572
  cluster_admin: Optional[bool] = Field(..., alias="clusterAdmin")
569
573
  image_patterns: list[str] = Field(..., alias="imagePatterns")
@@ -67,8 +67,19 @@ def slack_notify(
67
67
  in_progress: bool,
68
68
  trigger_integration: Optional[str] = None,
69
69
  trigger_reason: Optional[str] = None,
70
+ skip_successful_notifications: Optional[bool] = False,
70
71
  ) -> None:
71
72
  success = not ri.has_error_registered()
73
+ # if the deployment doesn't want any notifications for successful
74
+ # deployments, then we should grant the wish. However, there's a user
75
+ # expereince concern where the deployment owners will receive a "in
76
+ # progress" notice but no subsequent notice. We handle this case by
77
+ # including an "fyi" message for in progress deployments down below.
78
+ if success and skip_successful_notifications and not in_progress:
79
+ logging.info(
80
+ f"Skipping Slack notification for {saas_file_name} to {env_name} because deploy was successful."
81
+ )
82
+ return
72
83
  if in_progress:
73
84
  icon = ":yellow_jenkins_circle:"
74
85
  description = "In Progress"
@@ -87,6 +98,8 @@ def slack_notify(
87
98
  message += f". Reason: {trigger_reason}"
88
99
  if trigger_integration:
89
100
  message += f" triggered by _{trigger_integration}_"
101
+ if in_progress and skip_successful_notifications:
102
+ message += ". There will not be a notice for success."
90
103
  slack.chat_post_message(message)
91
104
 
92
105
 
@@ -120,6 +133,9 @@ def run(
120
133
  # - this is not a dry run
121
134
  # - there is a single saas file deployed
122
135
  notify = not dry_run and len(saas_files) == 1
136
+ skip_successful_deploy_notifications = (
137
+ saas_files[0].skip_successful_deploy_notifications if saas_files else False
138
+ )
123
139
  slack = None
124
140
  if notify:
125
141
  saas_file = saas_files[0]
@@ -151,6 +167,7 @@ def run(
151
167
  in_progress=False,
152
168
  trigger_integration=trigger_integration,
153
169
  trigger_reason=trigger_reason,
170
+ skip_successful_notifications=skip_successful_deploy_notifications,
154
171
  )
155
172
  )
156
173
  # deployment start notification
@@ -164,6 +181,7 @@ def run(
164
181
  in_progress=True,
165
182
  trigger_integration=trigger_integration,
166
183
  trigger_reason=trigger_reason,
184
+ skip_successful_notifications=skip_successful_deploy_notifications,
167
185
  )
168
186
 
169
187
  jenkins_map = jenkins_base.get_jenkins_map()
@@ -1,12 +1,20 @@
1
1
  from collections.abc import Callable
2
+ from unittest.mock import create_autospec
2
3
 
3
4
  import pytest
4
5
 
5
- from reconcile.openshift_saas_deploy import compose_console_url
6
+ from reconcile.openshift_saas_deploy import (
7
+ compose_console_url,
8
+ slack_notify,
9
+ )
6
10
  from reconcile.openshift_tekton_resources import (
7
11
  OpenshiftTektonResourcesNameTooLongError,
8
12
  )
9
13
  from reconcile.typed_queries.saas_files import SaasFile
14
+ from reconcile.utils import (
15
+ openshift_resource,
16
+ slack_api,
17
+ )
10
18
 
11
19
 
12
20
  @pytest.fixture
@@ -86,3 +94,94 @@ def test_compose_console_url_with_long_saas_name(
86
94
  f"Pipeline name o-saas-deploy-{saas_name} is longer than 56 characters"
87
95
  == str(e.value)
88
96
  )
97
+
98
+
99
+ def test_slack_notify_skipped_success():
100
+ api = create_autospec(slack_api.SlackApi)
101
+ slack_notify(
102
+ saas_file_name="test-slack_notify--skipped-success.yaml",
103
+ env_name="test",
104
+ slack=api,
105
+ ri=openshift_resource.ResourceInventory(),
106
+ console_url="https://test.local/console",
107
+ in_progress=False,
108
+ skip_successful_notifications=True,
109
+ )
110
+ api.chat_post_message.assert_not_called()
111
+
112
+
113
+ def test_slack_notify_unskipped_success():
114
+ api = create_autospec(slack_api.SlackApi)
115
+ slack_notify(
116
+ saas_file_name="test-slack_notify--unskipped-success.yaml",
117
+ env_name="test",
118
+ slack=api,
119
+ ri=openshift_resource.ResourceInventory(),
120
+ console_url="https://test.local/console",
121
+ in_progress=False,
122
+ skip_successful_notifications=False,
123
+ )
124
+ api.chat_post_message.assert_called_once_with(
125
+ ":green_jenkins_circle: SaaS file *test-slack_notify--unskipped-success.yaml* "
126
+ "deployment to environment *test*: Success "
127
+ "(<https://test.local/console|Open>)"
128
+ )
129
+
130
+
131
+ def test_slack_notify_unskipped_failure():
132
+ api = create_autospec(slack_api.SlackApi)
133
+ ri = openshift_resource.ResourceInventory()
134
+ ri.register_error()
135
+ slack_notify(
136
+ saas_file_name="test-saas-file-name.yaml",
137
+ env_name="test",
138
+ slack=api,
139
+ ri=ri,
140
+ console_url="https://test.local/console",
141
+ in_progress=False,
142
+ skip_successful_notifications=False,
143
+ )
144
+ api.chat_post_message.assert_called_once_with(
145
+ ":red_jenkins_circle: SaaS file *test-saas-file-name.yaml* "
146
+ "deployment to environment *test*: Failure "
147
+ "(<https://test.local/console|Open>)"
148
+ )
149
+
150
+
151
+ def test_slack_notify_skipped_failure():
152
+ api = create_autospec(slack_api.SlackApi)
153
+ ri = openshift_resource.ResourceInventory()
154
+ ri.register_error()
155
+ slack_notify(
156
+ saas_file_name="test-saas-file-name.yaml",
157
+ env_name="test",
158
+ slack=api,
159
+ ri=ri,
160
+ console_url="https://test.local/console",
161
+ in_progress=False,
162
+ skip_successful_notifications=True,
163
+ )
164
+ api.chat_post_message.assert_called_once_with(
165
+ ":red_jenkins_circle: SaaS file *test-saas-file-name.yaml* "
166
+ "deployment to environment *test*: Failure "
167
+ "(<https://test.local/console|Open>)"
168
+ )
169
+
170
+
171
+ def test_slack_notify_skipped_in_progress():
172
+ api = create_autospec(slack_api.SlackApi)
173
+ ri = openshift_resource.ResourceInventory()
174
+ slack_notify(
175
+ saas_file_name="test-saas-file-name.yaml",
176
+ env_name="test",
177
+ slack=api,
178
+ ri=ri,
179
+ console_url="https://test.local/console",
180
+ in_progress=True,
181
+ skip_successful_notifications=True,
182
+ )
183
+ api.chat_post_message.assert_called_once_with(
184
+ ":yellow_jenkins_circle: SaaS file *test-saas-file-name.yaml* "
185
+ "deployment to environment *test*: In Progress "
186
+ "(<https://test.local/console|Open>). There will not be a notice for success."
187
+ )
@@ -116,6 +116,9 @@ class SaasFile(ConfiguredBaseModel):
116
116
  deprecated: Optional[bool] = Field(..., alias="deprecated")
117
117
  compare: Optional[bool] = Field(..., alias="compare")
118
118
  timeout: Optional[str] = Field(..., alias="timeout")
119
+ skip_successful_deploy_notifications: Optional[bool] = Field(
120
+ ..., alias="skipSuccessfulDeployNotifications"
121
+ )
119
122
  publish_job_logs: Optional[bool] = Field(..., alias="publishJobLogs")
120
123
  cluster_admin: Optional[bool] = Field(..., alias="clusterAdmin")
121
124
  image_patterns: list[str] = Field(..., alias="imagePatterns")
@@ -7,7 +7,16 @@ def make_semver(major: int, minor: int, patch: int) -> str:
7
7
  return str(semver.VersionInfo(major=major, minor=minor, patch=patch))
8
8
 
9
9
 
10
- def parse_semver(version: str) -> semver.VersionInfo:
10
+ def parse_semver(
11
+ version: str, optional_minor_and_patch: bool = False
12
+ ) -> semver.VersionInfo:
13
+ if optional_minor_and_patch:
14
+ # semver3 supports optional minor and patch.
15
+ # until we upgrade to semver3, we support this by adding a default minor and/or patch
16
+ if "." not in version:
17
+ version = f"{version}.0.0"
18
+ elif version.count(".") == 1:
19
+ version = f"{version}.0"
11
20
  return semver.VersionInfo.parse(version)
12
21
 
13
22