qontract-reconcile 0.10.1rc879__py3-none-any.whl → 0.10.1rc894__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 (291) hide show
  1. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/METADATA +1 -1
  2. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/RECORD +291 -284
  3. reconcile/acs_rbac.py +1 -2
  4. reconcile/aus/advanced_upgrade_service.py +14 -14
  5. reconcile/aus/aus_label_source.py +1 -2
  6. reconcile/aus/base.py +23 -26
  7. reconcile/aus/cluster_version_data.py +4 -4
  8. reconcile/aus/models.py +2 -3
  9. reconcile/aus/version_gate_approver.py +2 -6
  10. reconcile/aus/version_gates/__init__.py +1 -3
  11. reconcile/aus/version_gates/sts_version_gate_handler.py +2 -3
  12. reconcile/aws_account_manager/integration.py +9 -14
  13. reconcile/aws_account_manager/reconciler.py +51 -1
  14. reconcile/aws_account_manager/utils.py +3 -0
  15. reconcile/aws_ami_cleanup/integration.py +3 -4
  16. reconcile/aws_iam_password_reset.py +2 -5
  17. reconcile/aws_version_sync/integration.py +2 -2
  18. reconcile/blackbox_exporter_endpoint_monitoring.py +2 -5
  19. reconcile/change_owners/approver.py +4 -5
  20. reconcile/change_owners/bundle.py +20 -22
  21. reconcile/change_owners/change_types.py +23 -24
  22. reconcile/change_owners/changes.py +13 -16
  23. reconcile/change_owners/decision.py +2 -5
  24. reconcile/change_owners/diff.py +11 -15
  25. reconcile/change_owners/self_service_roles.py +1 -2
  26. reconcile/change_owners/tester.py +7 -10
  27. reconcile/checkpoint.py +2 -5
  28. reconcile/cli.py +26 -12
  29. reconcile/closedbox_endpoint_monitoring_base.py +8 -11
  30. reconcile/cluster_deployment_mapper.py +2 -5
  31. reconcile/cna/assets/asset.py +4 -7
  32. reconcile/cna/assets/null.py +2 -5
  33. reconcile/cna/integration.py +2 -3
  34. reconcile/cna/state.py +2 -5
  35. reconcile/dashdotdb_base.py +8 -11
  36. reconcile/dashdotdb_cso.py +3 -6
  37. reconcile/dashdotdb_dora.py +10 -14
  38. reconcile/dashdotdb_dvo.py +10 -13
  39. reconcile/dashdotdb_slo.py +5 -8
  40. reconcile/database_access_manager.py +5 -6
  41. reconcile/dynatrace_token_provider/integration.py +3 -6
  42. reconcile/dynatrace_token_provider/integration_v2.py +20 -0
  43. reconcile/dynatrace_token_provider/meta.py +1 -0
  44. reconcile/external_resources/integration.py +1 -1
  45. reconcile/external_resources/manager.py +4 -4
  46. reconcile/external_resources/model.py +3 -3
  47. reconcile/external_resources/secrets_sync.py +5 -5
  48. reconcile/external_resources/state.py +5 -5
  49. reconcile/gabi_authorized_users.py +3 -6
  50. reconcile/gcr_mirror.py +1 -1
  51. reconcile/github_org.py +1 -3
  52. reconcile/github_repo_invites.py +2 -5
  53. reconcile/gitlab_housekeeping.py +7 -11
  54. reconcile/gitlab_labeler.py +1 -2
  55. reconcile/gitlab_members.py +2 -5
  56. reconcile/gitlab_permissions.py +1 -3
  57. reconcile/glitchtip/integration.py +5 -8
  58. reconcile/glitchtip_project_alerts/integration.py +57 -33
  59. reconcile/glitchtip_project_dsn/integration.py +8 -11
  60. reconcile/gql_definitions/aws_account_manager/aws_accounts.py +6 -0
  61. reconcile/gql_definitions/fragments/aws_account_managed.py +8 -0
  62. reconcile/gql_definitions/glitchtip/glitchtip_project.py +4 -4
  63. reconcile/gql_definitions/glitchtip_project_alerts/glitchtip_project.py +27 -7
  64. reconcile/integrations_manager.py +5 -8
  65. reconcile/jenkins/types.py +5 -6
  66. reconcile/jenkins_job_builder.py +9 -12
  67. reconcile/jenkins_roles.py +1 -1
  68. reconcile/jira_watcher.py +2 -2
  69. reconcile/ldap_groups/integration.py +2 -5
  70. reconcile/ocm/types.py +21 -26
  71. reconcile/ocm_addons_upgrade_tests_trigger.py +3 -6
  72. reconcile/ocm_clusters.py +8 -8
  73. reconcile/ocm_internal_notifications/integration.py +1 -2
  74. reconcile/ocm_labels/integration.py +2 -5
  75. reconcile/ocm_machine_pools.py +11 -15
  76. reconcile/ocm_upgrade_scheduler_org_updater.py +2 -5
  77. reconcile/openshift_base.py +29 -30
  78. reconcile/openshift_groups.py +15 -20
  79. reconcile/openshift_namespace_labels.py +8 -14
  80. reconcile/openshift_namespaces.py +5 -8
  81. reconcile/openshift_network_policies.py +2 -4
  82. reconcile/openshift_resources_base.py +19 -29
  83. reconcile/openshift_saas_deploy.py +9 -10
  84. reconcile/openshift_saas_deploy_change_tester.py +7 -10
  85. reconcile/openshift_saas_deploy_trigger_base.py +4 -7
  86. reconcile/openshift_saas_deploy_trigger_cleaner.py +5 -8
  87. reconcile/openshift_saas_deploy_trigger_configs.py +1 -2
  88. reconcile/openshift_saas_deploy_trigger_images.py +1 -2
  89. reconcile/openshift_saas_deploy_trigger_moving_commits.py +1 -2
  90. reconcile/openshift_saas_deploy_trigger_upstream_jobs.py +1 -2
  91. reconcile/openshift_tekton_resources.py +7 -11
  92. reconcile/openshift_upgrade_watcher.py +10 -13
  93. reconcile/openshift_users.py +8 -11
  94. reconcile/oum/base.py +3 -4
  95. reconcile/oum/labelset.py +1 -2
  96. reconcile/oum/metrics.py +2 -2
  97. reconcile/oum/models.py +1 -2
  98. reconcile/oum/standalone.py +2 -3
  99. reconcile/prometheus_rules_tester/integration.py +6 -9
  100. reconcile/quay_membership.py +1 -2
  101. reconcile/quay_mirror.py +12 -13
  102. reconcile/quay_mirror_org.py +10 -10
  103. reconcile/queries.py +4 -7
  104. reconcile/resource_scraper.py +3 -4
  105. reconcile/rhidp/common.py +2 -2
  106. reconcile/saas_auto_promotions_manager/integration.py +5 -6
  107. reconcile/saas_auto_promotions_manager/merge_request_manager/batcher.py +1 -2
  108. reconcile/saas_auto_promotions_manager/publisher.py +5 -6
  109. reconcile/saas_auto_promotions_manager/subscriber.py +36 -15
  110. reconcile/saas_auto_promotions_manager/utils/saas_files_inventory.py +8 -0
  111. reconcile/saas_file_validator.py +2 -5
  112. reconcile/signalfx_endpoint_monitoring.py +2 -5
  113. reconcile/skupper_network/integration.py +3 -6
  114. reconcile/skupper_network/models.py +3 -5
  115. reconcile/slack_base.py +4 -7
  116. reconcile/slack_usergroups.py +15 -17
  117. reconcile/sql_query.py +5 -9
  118. reconcile/status_board.py +4 -5
  119. reconcile/statuspage/atlassian.py +14 -15
  120. reconcile/statuspage/integrations/maintenances.py +3 -3
  121. reconcile/statuspage/page.py +8 -8
  122. reconcile/statuspage/state.py +4 -5
  123. reconcile/statuspage/status.py +7 -8
  124. reconcile/templating/lib/rendering.py +8 -8
  125. reconcile/templating/renderer.py +10 -11
  126. reconcile/templating/validator.py +4 -4
  127. reconcile/terraform_aws_route53.py +3 -6
  128. reconcile/terraform_cloudflare_dns.py +9 -12
  129. reconcile/terraform_cloudflare_resources.py +9 -11
  130. reconcile/terraform_cloudflare_users.py +8 -11
  131. reconcile/terraform_init/integration.py +2 -2
  132. reconcile/terraform_repo.py +11 -14
  133. reconcile/terraform_resources.py +20 -21
  134. reconcile/terraform_tgw_attachments.py +32 -36
  135. reconcile/terraform_users.py +6 -7
  136. reconcile/terraform_vpc_resources/integration.py +6 -6
  137. reconcile/test/conftest.py +7 -10
  138. reconcile/test/fixtures.py +1 -1
  139. reconcile/test/saas_auto_promotions_manager/conftest.py +3 -2
  140. reconcile/test/saas_auto_promotions_manager/merge_request_manager/renderer/conftest.py +2 -2
  141. reconcile/test/test_database_access_manager.py +3 -6
  142. reconcile/test/test_gitlab_labeler.py +2 -5
  143. reconcile/test/test_jump_host.py +5 -8
  144. reconcile/test/test_ocm_machine_pools.py +1 -4
  145. reconcile/test/test_openshift_base.py +3 -6
  146. reconcile/test/test_openshift_cluster_bots.py +5 -5
  147. reconcile/test/test_openshift_namespace_labels.py +2 -3
  148. reconcile/test/test_openshift_saas_deploy_trigger_cleaner.py +2 -2
  149. reconcile/test/test_saasherder.py +9 -12
  150. reconcile/test/test_slack_base.py +4 -6
  151. reconcile/test/test_status_board.py +4 -7
  152. reconcile/test/test_terraform_tgw_attachments.py +14 -20
  153. reconcile/typed_queries/alerting_services_settings.py +1 -2
  154. reconcile/typed_queries/app_interface_custom_messages.py +2 -3
  155. reconcile/typed_queries/app_interface_deadmanssnitch_settings.py +1 -3
  156. reconcile/typed_queries/app_interface_repo_url.py +1 -2
  157. reconcile/typed_queries/app_interface_state_settings.py +1 -3
  158. reconcile/typed_queries/app_interface_vault_settings.py +1 -2
  159. reconcile/typed_queries/aws_vpc_requests.py +1 -3
  160. reconcile/typed_queries/aws_vpcs.py +1 -3
  161. reconcile/typed_queries/clusters.py +2 -4
  162. reconcile/typed_queries/clusters_minimal.py +1 -3
  163. reconcile/typed_queries/clusters_with_dms.py +1 -3
  164. reconcile/typed_queries/dynatrace_environments.py +14 -0
  165. reconcile/typed_queries/external_resources.py +3 -4
  166. reconcile/typed_queries/pagerduty_instances.py +1 -2
  167. reconcile/typed_queries/repos.py +2 -3
  168. reconcile/typed_queries/reserved_networks.py +1 -3
  169. reconcile/typed_queries/saas_files.py +49 -59
  170. reconcile/typed_queries/slo_documents.py +1 -3
  171. reconcile/typed_queries/status_board.py +3 -7
  172. reconcile/typed_queries/tekton_pipeline_providers.py +1 -2
  173. reconcile/typed_queries/terraform_namespaces.py +1 -2
  174. reconcile/typed_queries/terraform_tgw_attachments/aws_accounts.py +1 -3
  175. reconcile/utils/acs/base.py +2 -3
  176. reconcile/utils/acs/notifiers.py +3 -3
  177. reconcile/utils/acs/policies.py +3 -3
  178. reconcile/utils/aggregated_list.py +1 -1
  179. reconcile/utils/amtool.py +1 -2
  180. reconcile/utils/aws_api.py +28 -31
  181. reconcile/utils/aws_api_typed/account.py +23 -0
  182. reconcile/utils/aws_api_typed/api.py +20 -9
  183. reconcile/utils/binary.py +1 -3
  184. reconcile/utils/clusterhealth/providerbase.py +1 -2
  185. reconcile/utils/clusterhealth/telemeter.py +2 -2
  186. reconcile/utils/deadmanssnitch_api.py +1 -2
  187. reconcile/utils/disabled_integrations.py +4 -6
  188. reconcile/utils/environ.py +1 -1
  189. reconcile/utils/expiration.py +3 -7
  190. reconcile/utils/external_resource_spec.py +3 -4
  191. reconcile/utils/external_resources.py +4 -7
  192. reconcile/utils/filtering.py +1 -2
  193. reconcile/utils/git.py +3 -9
  194. reconcile/utils/git_secrets.py +5 -5
  195. reconcile/utils/github_api.py +5 -9
  196. reconcile/utils/gitlab_api.py +2 -3
  197. reconcile/utils/glitchtip/client.py +2 -4
  198. reconcile/utils/glitchtip/models.py +8 -11
  199. reconcile/utils/gql.py +26 -35
  200. reconcile/utils/grouping.py +1 -3
  201. reconcile/utils/imap_client.py +2 -5
  202. reconcile/utils/internal_groups/client.py +1 -2
  203. reconcile/utils/internal_groups/models.py +8 -9
  204. reconcile/utils/jenkins_api.py +4 -4
  205. reconcile/utils/jinja2/extensions.py +1 -1
  206. reconcile/utils/jinja2/filters.py +4 -4
  207. reconcile/utils/jinja2/utils.py +16 -16
  208. reconcile/utils/jira_client.py +10 -11
  209. reconcile/utils/jjb_client.py +14 -17
  210. reconcile/utils/jobcontroller/controller.py +5 -5
  211. reconcile/utils/jobcontroller/models.py +2 -2
  212. reconcile/utils/jsonpath.py +4 -5
  213. reconcile/utils/jump_host.py +7 -8
  214. reconcile/utils/keycloak.py +3 -7
  215. reconcile/utils/ldap_client.py +2 -3
  216. reconcile/utils/lean_terraform_client.py +13 -17
  217. reconcile/utils/membershipsources/app_interface_resolver.py +1 -1
  218. reconcile/utils/membershipsources/models.py +19 -22
  219. reconcile/utils/metrics.py +13 -15
  220. reconcile/utils/mr/base.py +7 -11
  221. reconcile/utils/mr/glitchtip_access_reporter.py +2 -2
  222. reconcile/utils/mr/notificator.py +1 -2
  223. reconcile/utils/oc.py +38 -38
  224. reconcile/utils/oc_connection_parameters.py +24 -25
  225. reconcile/utils/oc_filters.py +2 -3
  226. reconcile/utils/oc_map.py +9 -15
  227. reconcile/utils/ocm/addons.py +7 -10
  228. reconcile/utils/ocm/base.py +38 -39
  229. reconcile/utils/ocm/clusters.py +6 -9
  230. reconcile/utils/ocm/label_sources.py +1 -2
  231. reconcile/utils/ocm/labels.py +3 -6
  232. reconcile/utils/ocm/ocm.py +11 -14
  233. reconcile/utils/ocm/products.py +1 -3
  234. reconcile/utils/ocm/search_filters.py +16 -17
  235. reconcile/utils/ocm/service_log.py +2 -3
  236. reconcile/utils/ocm/sre_capability_labels.py +4 -8
  237. reconcile/utils/ocm/subscriptions.py +1 -3
  238. reconcile/utils/ocm/syncsets.py +2 -4
  239. reconcile/utils/ocm/upgrades.py +5 -9
  240. reconcile/utils/ocm_base_client.py +13 -16
  241. reconcile/utils/openshift_resource.py +5 -11
  242. reconcile/utils/output.py +2 -3
  243. reconcile/utils/pagerduty_api.py +4 -5
  244. reconcile/utils/prometheus.py +2 -2
  245. reconcile/utils/promotion_state.py +4 -5
  246. reconcile/utils/promtool.py +2 -8
  247. reconcile/utils/quay_api.py +12 -22
  248. reconcile/utils/raw_github_api.py +3 -5
  249. reconcile/utils/rosa/rosa_cli.py +6 -6
  250. reconcile/utils/rosa/session.py +6 -7
  251. reconcile/utils/runtime/desired_state_diff.py +3 -8
  252. reconcile/utils/runtime/environment.py +4 -7
  253. reconcile/utils/runtime/integration.py +4 -4
  254. reconcile/utils/runtime/meta.py +1 -2
  255. reconcile/utils/runtime/runner.py +7 -10
  256. reconcile/utils/runtime/sharding.py +22 -27
  257. reconcile/utils/saasherder/interfaces.py +63 -69
  258. reconcile/utils/saasherder/models.py +30 -35
  259. reconcile/utils/saasherder/saasherder.py +39 -54
  260. reconcile/utils/secret_reader.py +17 -19
  261. reconcile/utils/slack_api.py +15 -17
  262. reconcile/utils/smtp_client.py +1 -2
  263. reconcile/utils/sqs_gateway.py +1 -3
  264. reconcile/utils/state.py +1 -2
  265. reconcile/utils/terraform/config_client.py +4 -5
  266. reconcile/utils/terraform_client.py +12 -8
  267. reconcile/utils/terrascript/cloudflare_client.py +4 -10
  268. reconcile/utils/terrascript/cloudflare_resources.py +10 -13
  269. reconcile/utils/terrascript/models.py +2 -3
  270. reconcile/utils/terrascript/resources.py +1 -2
  271. reconcile/utils/terrascript_aws_client.py +50 -38
  272. reconcile/utils/unleash/client.py +4 -7
  273. reconcile/utils/unleash/server.py +2 -2
  274. reconcile/utils/vault.py +8 -11
  275. reconcile/utils/vaultsecretref.py +2 -3
  276. reconcile/utils/vcs.py +7 -8
  277. reconcile/vault_replication.py +4 -8
  278. reconcile/vpc_peerings_validator.py +4 -9
  279. release/version.py +6 -7
  280. tools/app_interface_reporter.py +2 -2
  281. tools/cli_commands/gpg_encrypt.py +3 -6
  282. tools/cli_commands/systems_and_tools.py +4 -7
  283. tools/qontract_cli.py +105 -17
  284. tools/saas_promotion_state/__init__.py +0 -0
  285. tools/saas_promotion_state/saas_promotion_state.py +105 -0
  286. tools/template_validation.py +1 -1
  287. tools/test/conftest.py +45 -6
  288. tools/test/test_saas_promotion_state.py +187 -0
  289. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/WHEEL +0 -0
  290. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/entry_points.txt +0 -0
  291. {qontract_reconcile-0.10.1rc879.dist-info → qontract_reconcile-0.10.1rc894.dist-info}/top_level.txt +0 -0
@@ -7,6 +7,7 @@ import random
7
7
  import re
8
8
  import string
9
9
  import tempfile
10
+ from collections import Counter
10
11
  from collections.abc import (
11
12
  Iterable,
12
13
  Mapping,
@@ -21,7 +22,6 @@ from json import JSONDecodeError
21
22
  from threading import Lock
22
23
  from typing import (
23
24
  Any,
24
- Optional,
25
25
  cast,
26
26
  )
27
27
 
@@ -273,6 +273,18 @@ DEFAULT_TAGS = {
273
273
  }
274
274
 
275
275
 
276
+ class OutputResourceNameNotUniqueException(Exception):
277
+ def __init__(self, namespace, duplicates):
278
+ self.namespace, self.duplicates = namespace, duplicates
279
+ super().__init__(
280
+ str.format(
281
+ "Found duplicate values {} for 'output_resource_name' in namespace {}. Please ensure 'output_resource_name' is unique in a given namespace.",
282
+ self.duplicates,
283
+ self.namespace,
284
+ )
285
+ )
286
+
287
+
276
288
  class StateInaccessibleException(Exception):
277
289
  pass
278
290
 
@@ -379,9 +391,9 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
379
391
  integration_prefix: str,
380
392
  thread_pool_size: int,
381
393
  accounts: Iterable[dict[str, Any]],
382
- settings: Optional[Mapping[str, Any]] = None,
383
- prefetch_resources_by_schemas: Optional[list[str]] = None,
384
- secret_reader: Optional[SecretReaderBase] = None,
394
+ settings: Mapping[str, Any] | None = None,
395
+ prefetch_resources_by_schemas: list[str] | None = None,
396
+ secret_reader: SecretReaderBase | None = None,
385
397
  ) -> None:
386
398
  self.integration = integration
387
399
  self.integration_prefix = integration_prefix
@@ -478,9 +490,9 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
478
490
  self.rosa_authenticator_pre_signup_zip_lock = Lock()
479
491
  self.lambda_zip: dict[str, str] = {}
480
492
  self.lambda_lock = Lock()
481
- self.github: Optional[Github] = None
493
+ self.github: Github | None = None
482
494
  self.github_lock = Lock()
483
- self.gitlab: Optional[GitLabApi] = None
495
+ self.gitlab: GitLabApi | None = None
484
496
  self.gitlab_lock = Lock()
485
497
  self.jenkins_map: dict[str, JenkinsApi] = {}
486
498
  self.jenkins_lock = Lock()
@@ -509,7 +521,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
509
521
 
510
522
  # defaults from account
511
523
  bucket_backend_value = config.get("bucket")
512
- key_backend_value = config.get("{}_key".format(integration))
524
+ key_backend_value = config.get(f"{integration}_key")
513
525
  region_backend_value = config.get("region")
514
526
  terraform_state = config["terraformState"]
515
527
  if terraform_state:
@@ -757,7 +769,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
757
769
  self,
758
770
  roles,
759
771
  skip_reencrypt_accounts: list[str],
760
- appsre_pgp_key: Optional[str],
772
+ appsre_pgp_key: str | None,
761
773
  ):
762
774
  error = False
763
775
  for role in roles:
@@ -785,9 +797,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
785
797
 
786
798
  # we want to include the console url in the outputs
787
799
  # to be used later to generate the email invitations
788
- output_name = "{}_console-urls__{}".format(
789
- self.integration_prefix, account_name
790
- )
800
+ output_name = f"{self.integration_prefix}_console-urls__{account_name}"
791
801
  output_value = account_console_url
792
802
  tf_output = Output(output_name, value=output_value)
793
803
  self.add_resource(account_name, tf_output)
@@ -846,8 +856,8 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
846
856
  # we want the outputs to be formed into a mail invitation
847
857
  # for each new user. we form an output of the form
848
858
  # 'qrtf.enc-passwords[user_name] = <encrypted password>
849
- output_name = "{}_enc-passwords__{}".format(
850
- self.integration_prefix, user_name
859
+ output_name = (
860
+ f"{self.integration_prefix}_enc-passwords__{user_name}"
851
861
  )
852
862
  output_value = (
853
863
  "${" + tf_iam_user_login_profile.encrypted_password + "}"
@@ -891,7 +901,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
891
901
  self,
892
902
  roles,
893
903
  skip_reencrypt_accounts: list[str],
894
- appsre_pgp_key: Optional[str] = None,
904
+ appsre_pgp_key: str | None = None,
895
905
  ):
896
906
  self.populate_iam_groups(roles)
897
907
  err = self.populate_iam_users(
@@ -912,7 +922,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
912
922
  @staticmethod
913
923
  def get_resource_lifecycle(
914
924
  common_values: dict[str, Any],
915
- ) -> Optional[dict[str, Any]]:
925
+ ) -> dict[str, Any] | None:
916
926
  if lifecycle := common_values.get("lifecycle"):
917
927
  lifecycle = NamespaceTerraformResourceLifecycleV1(**lifecycle)
918
928
  if lifecycle.create_before_destroy is None:
@@ -1013,7 +1023,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
1013
1023
  record["health_check_id"] = f"${{{healthcheck_resource.id}}}"
1014
1024
 
1015
1025
  # Get value from Vault if _records_from_vault was set
1016
- records_from_vault: Optional[Iterable[dict[str, str]]] = record.pop(
1026
+ records_from_vault: Iterable[dict[str, str]] | None = record.pop(
1017
1027
  "records_from_vault", None
1018
1028
  )
1019
1029
  if records_from_vault:
@@ -1467,7 +1477,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
1467
1477
 
1468
1478
  return results
1469
1479
 
1470
- def populate_resources(self, ocm_map: Optional[OCMMap] = None) -> None:
1480
+ def populate_resources(self, ocm_map: OCMMap | None = None) -> None:
1471
1481
  """
1472
1482
  Populates the terraform configuration from resource specs.
1473
1483
  :param ocm_map:
@@ -1479,7 +1489,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
1479
1489
  def init_populate_specs(
1480
1490
  self,
1481
1491
  namespaces: Iterable[Mapping[str, Any]],
1482
- account_names: Optional[Iterable[str]],
1492
+ account_names: Iterable[str] | None,
1483
1493
  ) -> None:
1484
1494
  """
1485
1495
  Initiates resource specs from the definitions in app-interface
@@ -1494,12 +1504,19 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
1494
1504
  specs = get_external_resource_specs(
1495
1505
  namespace_info, provision_provider=PROVIDER_AWS
1496
1506
  )
1507
+ name_counter = Counter(spec.output_resource_name for spec in specs)
1508
+ duplicates = [name for name, count in name_counter.items() if count > 1]
1509
+ if duplicates:
1510
+ raise OutputResourceNameNotUniqueException(
1511
+ namespace_info.get("name"), duplicates
1512
+ )
1497
1513
  for spec in specs:
1498
1514
  if account_names and spec.provisioner_name not in account_names:
1499
1515
  continue
1500
1516
  self.account_resource_specs.setdefault(
1501
1517
  spec.provisioner_name, []
1502
1518
  ).append(spec)
1519
+
1503
1520
  self.resource_spec_inventory[spec.id_object()] = spec
1504
1521
 
1505
1522
  def populate_tf_resources(self, spec, ocm_map=None):
@@ -1908,7 +1925,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
1908
1925
 
1909
1926
  def _find_resource_spec(
1910
1927
  self, account: str, source: str, provider: str
1911
- ) -> Optional[ExternalResourceSpec]:
1928
+ ) -> ExternalResourceSpec | None:
1912
1929
  if account not in self.account_resource_specs:
1913
1930
  return None
1914
1931
 
@@ -2168,7 +2185,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
2168
2185
  tf_resources.append(Output(output_name, value=output_value))
2169
2186
  output_name = output_prefix + "__aws_region"
2170
2187
  tf_resources.append(Output(output_name, value=region))
2171
- endpoint = "s3.{}.amazonaws.com".format(region)
2188
+ endpoint = f"s3.{region}.amazonaws.com"
2172
2189
  output_name = output_prefix + "__endpoint"
2173
2190
  tf_resources.append(Output(output_name, value=endpoint))
2174
2191
 
@@ -2480,7 +2497,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
2480
2497
  for k, v in data.items():
2481
2498
  to_replace = "${" + k + "}"
2482
2499
  user_policy = user_policy.replace(to_replace, v)
2483
- output_name = output_prefix + "__{}".format(k)
2500
+ output_name = output_prefix + f"__{k}"
2484
2501
  tf_resources.append(Output(output_name, value=v))
2485
2502
 
2486
2503
  tf_aws_iam_policy = aws_iam_policy(
@@ -2769,10 +2786,8 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
2769
2786
  tf_resources.append(queue_tf_resource)
2770
2787
  output_name = output_prefix + "__aws_region"
2771
2788
  tf_resources.append(Output(output_name, value=region))
2772
- output_name = "{}__{}".format(output_prefix, queue_key)
2773
- output_value = "https://sqs.{}.amazonaws.com/{}/{}".format(
2774
- region, uid, queue_name
2775
- )
2789
+ output_name = f"{output_prefix}__{queue_key}"
2790
+ output_value = f"https://sqs.{region}.amazonaws.com/{uid}/{queue_name}"
2776
2791
  tf_resources.append(Output(output_name, value=output_value))
2777
2792
  all_queues_per_spec.append(all_queues)
2778
2793
 
@@ -2925,7 +2940,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
2925
2940
  values["provider"] = "aws." + region
2926
2941
  table_tf_resource = aws_dynamodb_table(table, **values)
2927
2942
  tf_resources.append(table_tf_resource)
2928
- output_name = "{}__{}".format(output_prefix, table_key)
2943
+ output_name = f"{output_prefix}__{table_key}"
2929
2944
  tf_resources.append(Output(output_name, value=table))
2930
2945
 
2931
2946
  output_name = output_prefix + "__aws_region"
@@ -2960,8 +2975,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
2960
2975
  "Effect": "Allow",
2961
2976
  "Action": ["dynamodb:*"],
2962
2977
  "Resource": [
2963
- "arn:aws:dynamodb:{}:{}:table/{}".format(region, uid, t)
2964
- for t in all_tables
2978
+ f"arn:aws:dynamodb:{region}:{uid}:table/{t}" for t in all_tables
2965
2979
  ],
2966
2980
  }
2967
2981
  ],
@@ -3420,10 +3434,8 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
3420
3434
  tf_resources.append(user_policy_attachment_tf_resource)
3421
3435
 
3422
3436
  # outputs
3423
- output_name = "{}__{}".format(output_prefix, sqs_identifier)
3424
- output_value = "https://sqs.{}.amazonaws.com/{}/{}".format(
3425
- region, uid, sqs_identifier
3426
- )
3437
+ output_name = f"{output_prefix}__{sqs_identifier}"
3438
+ output_value = f"https://sqs.{region}.amazonaws.com/{uid}/{sqs_identifier}"
3427
3439
  tf_resources.append(Output(output_name, value=output_value))
3428
3440
 
3429
3441
  self.add_resources(account, tf_resources)
@@ -4042,8 +4054,8 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
4042
4054
  def add_resource(self, account, tf_resource):
4043
4055
  if account not in self.locks:
4044
4056
  logging.debug(
4045
- "integration {} is disabled for account {}. "
4046
- "can not add resource".format(self.integration, account)
4057
+ f"integration {self.integration} is disabled for account {account}. "
4058
+ "can not add resource"
4047
4059
  )
4048
4060
  return
4049
4061
  with self.locks[account]:
@@ -4051,8 +4063,8 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
4051
4063
 
4052
4064
  def dump(
4053
4065
  self,
4054
- print_to_file: Optional[str] = None,
4055
- existing_dirs: Optional[dict[str, str]] = None,
4066
+ print_to_file: str | None = None,
4067
+ existing_dirs: dict[str, str] | None = None,
4056
4068
  ) -> dict[str, str]:
4057
4069
  """
4058
4070
  Dump the Terraform configurations (in JSON format) to the working directories.
@@ -4311,7 +4323,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
4311
4323
 
4312
4324
  def _get_elasticsearch_account_wide_resource_policy(
4313
4325
  self, account: str
4314
- ) -> Optional[aws_cloudwatch_log_resource_policy]:
4326
+ ) -> aws_cloudwatch_log_resource_policy | None:
4315
4327
  """
4316
4328
  https://docs.aws.amazon.com/opensearch-service/latest/developerguide/createdomain-configure-slow-logs.html
4317
4329
  CloudWatch Logs supports 10 resource policies per Region.
@@ -5386,7 +5398,7 @@ class TerrascriptClient: # pylint: disable=too-many-public-methods
5386
5398
 
5387
5399
  def get_asg_image_id(
5388
5400
  self, filters: Iterable[Mapping[str, Any]], account: str, region: str
5389
- ) -> Optional[str]:
5401
+ ) -> str | None:
5390
5402
  """
5391
5403
  AMI ID comes form AWS Api filter result.
5392
5404
  AMI needs to be shared by integration aws-ami-share.
@@ -2,10 +2,7 @@ import logging
2
2
  import os
3
3
  import threading
4
4
  from collections.abc import Mapping
5
- from typing import (
6
- Any,
7
- Optional,
8
- )
5
+ from typing import Any
9
6
 
10
7
  from UnleashClient import (
11
8
  BaseCache,
@@ -13,7 +10,7 @@ from UnleashClient import (
13
10
  )
14
11
  from UnleashClient.strategies import Strategy
15
12
 
16
- client: Optional[UnleashClient] = None
13
+ client: UnleashClient | None = None
17
14
  client_lock = threading.Lock()
18
15
 
19
16
 
@@ -43,7 +40,7 @@ class ClusterStrategy(Strategy):
43
40
 
44
41
 
45
42
  class DisableClusterStrategy(ClusterStrategy):
46
- def apply(self, context: Optional[dict] = None) -> bool:
43
+ def apply(self, context: dict | None = None) -> bool:
47
44
  enable = True
48
45
 
49
46
  if context and "cluster_name" in context.keys():
@@ -54,7 +51,7 @@ class DisableClusterStrategy(ClusterStrategy):
54
51
 
55
52
 
56
53
  class EnableClusterStrategy(ClusterStrategy):
57
- def apply(self, context: Optional[dict] = None) -> bool:
54
+ def apply(self, context: dict | None = None) -> bool:
58
55
  enable = False
59
56
 
60
57
  if context and "cluster_name" in context.keys():
@@ -1,4 +1,4 @@
1
- from enum import Enum
1
+ from enum import StrEnum
2
2
 
3
3
  import requests
4
4
  from pydantic import BaseModel, Field
@@ -6,7 +6,7 @@ from pydantic import BaseModel, Field
6
6
  from reconcile.utils.rest_api_base import ApiBase, BearerTokenAuth
7
7
 
8
8
 
9
- class FeatureToggleType(str, Enum):
9
+ class FeatureToggleType(StrEnum):
10
10
  experiment = "experiment"
11
11
  kill_switch = "kill-switch"
12
12
  release = "release"
reconcile/utils/vault.py CHANGED
@@ -5,7 +5,6 @@ import threading
5
5
  import time
6
6
  from collections.abc import Mapping
7
7
  from functools import lru_cache
8
- from typing import Optional
9
8
 
10
9
  import hvac
11
10
  import requests
@@ -60,11 +59,11 @@ class _VaultClient:
60
59
 
61
60
  def __init__(
62
61
  self,
63
- server: Optional[str] = None,
64
- role_id: Optional[str] = None,
65
- secret_id: Optional[str] = None,
66
- kube_auth_role: Optional[str] = None,
67
- kube_auth_mount: Optional[str] = None,
62
+ server: str | None = None,
63
+ role_id: str | None = None,
64
+ secret_id: str | None = None,
65
+ kube_auth_role: str | None = None,
66
+ kube_auth_mount: str | None = None,
68
67
  auto_refresh: bool = True,
69
68
  ):
70
69
  config = get_config()
@@ -164,7 +163,7 @@ class _VaultClient:
164
163
  self._client.auth_approle(self.role_id, self.secret_id)
165
164
 
166
165
  @retry()
167
- def read_all_with_version(self, secret: Mapping) -> tuple[Mapping, Optional[str]]:
166
+ def read_all_with_version(self, secret: Mapping) -> tuple[Mapping, str | None]:
168
167
  """Returns a dictionary of keys and values in a Vault secret and the
169
168
  version of the secret, for V1 secrets, version will be None.
170
169
 
@@ -215,9 +214,7 @@ class _VaultClient:
215
214
 
216
215
  return version
217
216
 
218
- def __read_all_v2(
219
- self, path: str, version: Optional[str]
220
- ) -> tuple[dict, Optional[str]]:
217
+ def __read_all_v2(self, path: str, version: str | None) -> tuple[dict, str | None]:
221
218
  path_split = path.split("/")
222
219
  mount_point = path_split[0]
223
220
  read_path = "/".join(path_split[1:])
@@ -306,7 +303,7 @@ class _VaultClient:
306
303
  try:
307
304
  secret_field = data[field]
308
305
  except KeyError:
309
- raise SecretFieldNotFound("{}/{}".format(path, field))
306
+ raise SecretFieldNotFound(f"{path}/{field}")
310
307
  return secret_field
311
308
 
312
309
  @retry()
@@ -1,6 +1,5 @@
1
1
  from dataclasses import dataclass
2
2
  from typing import (
3
- Optional,
4
3
  cast,
5
4
  )
6
5
 
@@ -16,8 +15,8 @@ class VaultSecretRef:
16
15
 
17
16
  path: str
18
17
  field: str
19
- format: Optional[str] = None
20
- version: Optional[int] = None
18
+ format: str | None = None
19
+ version: int | None = None
21
20
 
22
21
  def get(self, field=None, default=None):
23
22
  secret_content = self._resolve_secret()
reconcile/utils/vcs.py CHANGED
@@ -6,7 +6,6 @@ from collections.abc import Iterable
6
6
  from dataclasses import dataclass
7
7
  from datetime import datetime
8
8
  from enum import Enum
9
- from typing import Optional
10
9
 
11
10
  from gitlab.v4.objects import ProjectMergeRequest
12
11
 
@@ -57,10 +56,10 @@ class VCS:
57
56
  dry_run: bool,
58
57
  allow_deleting_mrs: bool = False,
59
58
  allow_opening_mrs: bool = False,
60
- gitlab_instance: Optional[GitLabApi] = None,
61
- default_gh_token: Optional[str] = None,
62
- app_interface_api: Optional[GitLabApi] = None,
63
- github_api_per_repo_url: Optional[dict[str, GithubRepositoryApi]] = None,
59
+ gitlab_instance: GitLabApi | None = None,
60
+ default_gh_token: str | None = None,
61
+ app_interface_api: GitLabApi | None = None,
62
+ github_api_per_repo_url: dict[str, GithubRepositoryApi] | None = None,
64
63
  ):
65
64
  self._dry_run = dry_run
66
65
  self._allow_deleting_mrs = allow_deleting_mrs
@@ -106,7 +105,7 @@ class VCS:
106
105
  return defaults[0]
107
106
 
108
107
  def _init_github(
109
- self, repo_url: str, auth_code: Optional[HasSecret]
108
+ self, repo_url: str, auth_code: HasSecret | None
110
109
  ) -> GithubRepositoryApi:
111
110
  if repo_url not in self._gh_per_repo_url:
112
111
  if auth_code:
@@ -159,7 +158,7 @@ class VCS:
159
158
  return MRCheckStatus.NONE
160
159
 
161
160
  def get_commit_sha(
162
- self, repo_url: str, ref: str, auth_code: Optional[HasSecret]
161
+ self, repo_url: str, ref: str, auth_code: HasSecret | None
163
162
  ) -> str:
164
163
  if bool(self._is_commit_sha_regex.search(ref)):
165
164
  return ref
@@ -174,7 +173,7 @@ class VCS:
174
173
  repo_url: str,
175
174
  commit_from: str,
176
175
  commit_to: str,
177
- auth_code: Optional[HasSecret],
176
+ auth_code: HasSecret | None,
178
177
  ) -> list[Commit]:
179
178
  """
180
179
  Return a list of commits between two commits.
@@ -2,8 +2,6 @@ import logging
2
2
  import re
3
3
  from collections.abc import Iterable
4
4
  from typing import (
5
- Optional,
6
- Union,
7
5
  cast,
8
6
  )
9
7
 
@@ -179,7 +177,7 @@ def copy_vault_secret(
179
177
 
180
178
  def check_invalid_paths(
181
179
  path_list: Iterable[str],
182
- policy_paths: Optional[Iterable[str]],
180
+ policy_paths: Iterable[str] | None,
183
181
  ) -> None:
184
182
  """Checks if the paths to be replicated are present in the policy used to limit the secrets
185
183
  that are going to be replicated."""
@@ -283,12 +281,10 @@ def get_jenkins_secret_list(
283
281
 
284
282
 
285
283
  def get_vault_credentials(
286
- vault_auth: Union[
287
- VaultReplicationConfigV1_VaultInstanceAuthV1,
288
- VaultInstanceV1_VaultReplicationConfigV1_VaultInstanceAuthV1,
289
- ],
284
+ vault_auth: VaultReplicationConfigV1_VaultInstanceAuthV1
285
+ | VaultInstanceV1_VaultReplicationConfigV1_VaultInstanceAuthV1,
290
286
  vault_address: str,
291
- ) -> dict[str, Optional[str]]:
287
+ ) -> dict[str, str | None]:
292
288
  """Returns a dictionary with the credentials used to authenticate with Vault,
293
289
  retrieved from the values present on AppInterface and comming from Vault itself."""
294
290
  vault_creds = {}
@@ -2,7 +2,6 @@ import ipaddress
2
2
  import logging
3
3
  import sys
4
4
  from typing import (
5
- Union,
6
5
  cast,
7
6
  )
8
7
 
@@ -107,10 +106,8 @@ def validate_no_internal_to_public_peerings(
107
106
  }:
108
107
  continue
109
108
  connection = cast(
110
- Union[
111
- ClusterPeeringConnectionClusterAccepterV1,
112
- ClusterPeeringConnectionClusterRequesterV1,
113
- ],
109
+ ClusterPeeringConnectionClusterAccepterV1
110
+ | ClusterPeeringConnectionClusterRequesterV1,
114
111
  connection,
115
112
  )
116
113
  peer = connection.cluster
@@ -150,10 +147,8 @@ def validate_no_public_to_public_peerings(
150
147
  }:
151
148
  continue
152
149
  connection = cast(
153
- Union[
154
- ClusterPeeringConnectionClusterAccepterV1,
155
- ClusterPeeringConnectionClusterRequesterV1,
156
- ],
150
+ ClusterPeeringConnectionClusterAccepterV1
151
+ | ClusterPeeringConnectionClusterRequesterV1,
157
152
  connection,
158
153
  )
159
154
  peer = connection.cluster
release/version.py CHANGED
@@ -5,7 +5,6 @@ import os
5
5
  import re
6
6
  import subprocess
7
7
  import sys
8
- from subprocess import PIPE
9
8
  from typing import Optional
10
9
 
11
10
  GIT_VERSION_FILE = "GIT_VERSION"
@@ -21,7 +20,7 @@ def git() -> str:
21
20
  """
22
21
  cmd = "git describe --tags --match=[0-9]*.[0-9]*.[0-9]*"
23
22
  try:
24
- p = subprocess.run(cmd.split(" "), stdout=PIPE, stderr=PIPE, check=True)
23
+ p = subprocess.run(cmd.split(" "), capture_output=True, check=True)
25
24
  v = p.stdout.decode("utf-8").strip()
26
25
  # tox is running setup.py sdist from the git repo, and then runs again outside
27
26
  # of the git repo. At this second step, we cannot run git commands.
@@ -35,7 +34,7 @@ def git() -> str:
35
34
  # if we're not in a git repo, try reading out from the GIT_VERSION file
36
35
  if os.path.exists(GIT_VERSION_FILE):
37
36
  with open(
38
- GIT_VERSION_FILE, "r", encoding=locale.getpreferredencoding(False)
37
+ GIT_VERSION_FILE, encoding=locale.getpreferredencoding(False)
39
38
  ) as f:
40
39
  return f.read()
41
40
  print(e.stderr)
@@ -45,11 +44,11 @@ def git() -> str:
45
44
  def commit(length: int = 7) -> str:
46
45
  """get the current git commitid"""
47
46
  cmd = f"git rev-parse --short={length} HEAD"
48
- p = subprocess.run(cmd.split(" "), stdout=PIPE, stderr=PIPE, check=True)
47
+ p = subprocess.run(cmd.split(" "), capture_output=True, check=True)
49
48
  return p.stdout.decode("utf-8").strip()
50
49
 
51
50
 
52
- def semver(git_version: Optional[str] = None) -> str:
51
+ def semver(git_version: Optional[str] = None) -> str: # noqa: UP007 - RHEL8 has python 3.8
53
52
  """get a semantic version out of the input git version (see git())
54
53
  - if a X.Y.Z tag is set on the current HEAD, we'll use this
55
54
  - else we'll use X.Y.<Z+1>-<count>+<commitid> to respect semver and version
@@ -72,7 +71,7 @@ def semver(git_version: Optional[str] = None) -> str:
72
71
  return str(v)
73
72
 
74
73
 
75
- def pip(git_version: Optional[str] = None) -> str:
74
+ def pip(git_version: Optional[str] = None) -> str: # noqa: UP007 - RHEL8 has python 3.8
76
75
  """get a pip version out of the input git version (see git()),
77
76
  according to https://peps.python.org/pep-0440/
78
77
  - if a X.Y.Z tag is set on the current HEAD, we'll use this
@@ -88,7 +87,7 @@ def pip(git_version: Optional[str] = None) -> str:
88
87
  # return str(v)
89
88
 
90
89
 
91
- def docker(git_version: Optional[str] = None) -> str:
90
+ def docker(git_version: Optional[str] = None) -> str: # noqa: UP007 - RHEL8 has python 3.8
92
91
  # docker tags don't like '+' characters, let's remove the buildinfo/commitid
93
92
  return pip(git_version)
94
93
 
@@ -2,8 +2,8 @@ import logging
2
2
  import os
3
3
  import textwrap
4
4
  from datetime import (
5
+ UTC,
5
6
  datetime,
6
- timezone,
7
7
  )
8
8
 
9
9
  import click
@@ -221,7 +221,7 @@ def get_apps_data(date, month_delta=1, thread_pool_size=10):
221
221
  jjb: JJB = init_jjb(secret_reader)
222
222
  jenkins_map = jenkins_base.get_jenkins_map()
223
223
  time_limit = date - relativedelta(months=month_delta)
224
- timestamp_limit = int(time_limit.replace(tzinfo=timezone.utc).timestamp())
224
+ timestamp_limit = int(time_limit.replace(tzinfo=UTC).timestamp())
225
225
 
226
226
  secret_content = secret_reader.read_all({"path": DASHDOTDB_SECRET})
227
227
  dashdotdb_url = secret_content["url"]
@@ -1,11 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ from collections.abc import Mapping
4
5
  from dataclasses import dataclass
5
- from typing import (
6
- Mapping,
7
- Optional,
8
- )
9
6
 
10
7
  from reconcile import queries
11
8
  from reconcile.utils import (
@@ -114,7 +111,7 @@ class GPGEncryptCommand:
114
111
  f"No argument given which defines how to fetch the secret {self._command_data}"
115
112
  )
116
113
 
117
- def _get_gpg_key(self) -> Optional[str]:
114
+ def _get_gpg_key(self) -> str | None:
118
115
  target_user = self._command_data.target_user
119
116
  users = queries.get_users_by(
120
117
  refs=False,
@@ -156,7 +153,7 @@ class GPGEncryptCommand:
156
153
  def create(
157
154
  cls,
158
155
  command_data: GPGEncryptCommandData,
159
- secret_reader: Optional[SecretReader] = None,
156
+ secret_reader: SecretReader | None = None,
160
157
  ) -> GPGEncryptCommand:
161
158
  cls_secret_reader = (
162
159
  secret_reader
@@ -3,7 +3,6 @@
3
3
 
4
4
  from typing import (
5
5
  Any,
6
- Optional,
7
6
  Self,
8
7
  )
9
8
 
@@ -145,7 +144,7 @@ class SystemTool(BaseModel):
145
144
 
146
145
  @classmethod
147
146
  def init_from_model(
148
- cls, model: Any, enumeration: Any, parent: Optional[str] = None
147
+ cls, model: Any, enumeration: Any, parent: str | None = None
149
148
  ) -> Self:
150
149
  match model:
151
150
  case GitlabInstanceV1():
@@ -284,7 +283,7 @@ class SystemTool(BaseModel):
284
283
  cls,
285
284
  c: OpenShiftClusterManagerUpgradePolicyClusterV1,
286
285
  enumeration: Any,
287
- parent: Optional[str] = None,
286
+ parent: str | None = None,
288
287
  ) -> Self:
289
288
  return cls(
290
289
  system_type="openshift",
@@ -394,15 +393,13 @@ class SystemToolInventory:
394
393
  def __init__(self) -> None:
395
394
  self.systems_and_tools: list[SystemTool] = []
396
395
 
397
- def append(
398
- self, model: Any, enumeration: Any, parent: Optional[str] = None
399
- ) -> None:
396
+ def append(self, model: Any, enumeration: Any, parent: str | None = None) -> None:
400
397
  self.systems_and_tools.append(
401
398
  SystemTool.init_from_model(model, enumeration, parent=parent)
402
399
  )
403
400
 
404
401
  def update(
405
- self, models: list[Any], enumeration: Any, parent: Optional[str] = None
402
+ self, models: list[Any], enumeration: Any, parent: str | None = None
406
403
  ) -> None:
407
404
  for m in models:
408
405
  self.append(m, enumeration, parent=parent)