pycfmodel 2.0.0__tar.gz → 2.0.1__tar.gz

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 (135) hide show
  1. {pycfmodel-2.0.0/pycfmodel.egg-info → pycfmodel-2.0.1}/PKG-INFO +1 -1
  2. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/cloudformation_actions.py +7 -0
  3. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/autoscaling_auto_scaling_group.py +16 -2
  4. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/cloudwatch_alarm.py +1 -12
  5. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/dynamodb_table.py +23 -17
  6. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/elbv2_target_group.py +2 -13
  7. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/iam_group.py +1 -1
  8. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/iam_user.py +1 -1
  9. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/s3_bucket.py +15 -3
  10. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/sns_topic.py +7 -12
  11. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/sqs_queue.py +3 -12
  12. {pycfmodel-2.0.0 → pycfmodel-2.0.1/pycfmodel.egg-info}/PKG-INFO +1 -1
  13. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/scripts/generate_resource_from_schema.py +137 -35
  14. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_autoscaling_auto_scaling_group.py +35 -0
  15. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_elbv2_target_group.py +2 -1
  16. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_s3_bucket.py +51 -0
  17. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_sns_topic.py +2 -1
  18. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_sqs_queue.py +2 -1
  19. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_cf_model.py +0 -1
  20. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.flake8 +0 -0
  21. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/dependabot.yml +0 -0
  22. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/pull_request_template.md +0 -0
  23. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/release-drafter.yml +0 -0
  24. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/workflows/copilot-setup-steps.yml +0 -0
  25. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/workflows/lint-and-test.yml +0 -0
  26. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/workflows/pypi-release.yml +0 -0
  27. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/workflows/release-drafter.yml +0 -0
  28. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/workflows/test-docs.yml +0 -0
  29. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/workflows/update-cloudformation-actions.yml +0 -0
  30. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.github/workflows/zizmor.yml +0 -0
  31. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.gitignore +0 -0
  32. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/.readthedocs.yaml +0 -0
  33. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/CHANGELOG.md +0 -0
  34. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/CLAUDE.md +0 -0
  35. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/CODE_OF_CONDUCT.md +0 -0
  36. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/CONTRIBUTING.md +0 -0
  37. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/LICENSE.md +0 -0
  38. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/Makefile +0 -0
  39. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/README.md +0 -0
  40. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/docs/changelog.md +0 -0
  41. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/docs/code_of_conduct.md +0 -0
  42. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/docs/contributing.md +0 -0
  43. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/docs/index.md +0 -0
  44. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/docs/reference/cfmodel.md +0 -0
  45. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/docs/reference/properties.md +0 -0
  46. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/docs/reference/resources.md +0 -0
  47. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/docs/stylesheets/extra.css +0 -0
  48. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/mkdocs.yml +0 -0
  49. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/__init__.py +0 -0
  50. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/action_expander.py +0 -0
  51. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/constants.py +0 -0
  52. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/base.py +0 -0
  53. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/cf_model.py +0 -0
  54. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/generic.py +0 -0
  55. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/parameter.py +0 -0
  56. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/ec2_vpc_endpoint_policy.py +0 -0
  57. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/elbv2_listener.py +0 -0
  58. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/es_domain.py +0 -0
  59. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/generic_resource.py +0 -0
  60. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/iam_instance_profile.py +0 -0
  61. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/iam_managed_policy.py +0 -0
  62. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/iam_policy.py +0 -0
  63. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/iam_role.py +0 -0
  64. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/kms_key.py +0 -0
  65. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/opensearch_domain.py +0 -0
  66. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/policy.py +0 -0
  67. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/policy_document.py +0 -0
  68. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/property.py +0 -0
  69. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/security_group_egress_prop.py +0 -0
  70. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/security_group_ingress_prop.py +0 -0
  71. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/statement.py +0 -0
  72. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/statement_condition.py +0 -0
  73. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/tag.py +0 -0
  74. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/properties/types.py +0 -0
  75. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/resource.py +0 -0
  76. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/route53_record_set.py +0 -0
  77. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/s3_bucket_policy.py +0 -0
  78. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/security_group.py +0 -0
  79. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/security_group_egress.py +0 -0
  80. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/security_group_ingress.py +0 -0
  81. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/sns_subscription.py +0 -0
  82. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/sns_topic_policy.py +0 -0
  83. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/sqs_queue_policy.py +0 -0
  84. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/types.py +0 -0
  85. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/resources/wafv2_ip_set.py +0 -0
  86. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/types.py +0 -0
  87. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/model/utils.py +0 -0
  88. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/resolver.py +0 -0
  89. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel/utils.py +0 -0
  90. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel.egg-info/SOURCES.txt +0 -0
  91. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel.egg-info/dependency_links.txt +0 -0
  92. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel.egg-info/requires.txt +0 -0
  93. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pycfmodel.egg-info/top_level.txt +0 -0
  94. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/pyproject.toml +0 -0
  95. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/scripts/generate_cloudformation_actions_file.py +0 -0
  96. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/setup.cfg +0 -0
  97. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/properties/test_policy_document.py +0 -0
  98. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/properties/test_principal.py +0 -0
  99. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/properties/test_statement.py +0 -0
  100. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/properties/test_statement_condition.py +0 -0
  101. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_cloudwatch_alarm.py +0 -0
  102. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_dynamodb_table.py +0 -0
  103. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_ec2_vpc_endpoint_policy.py +0 -0
  104. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_elbv2_listener.py +0 -0
  105. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_es_domain.py +0 -0
  106. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_generic_resoure.py +0 -0
  107. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_iam_group.py +0 -0
  108. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_iam_instance_profile.py +0 -0
  109. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_iam_managed_policy.py +0 -0
  110. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_iam_policy.py +0 -0
  111. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_iam_role.py +0 -0
  112. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_iam_user.py +0 -0
  113. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_kms_key.py +0 -0
  114. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_opensearch_domain.py +0 -0
  115. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_rds_security_groups.py +0 -0
  116. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_route53_record_set.py +0 -0
  117. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_s3_bucket_policy.py +0 -0
  118. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_security_group.py +0 -0
  119. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_security_group_egress.py +0 -0
  120. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_security_group_ingress.py +0 -0
  121. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_sns_subscription.py +0 -0
  122. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_sns_topic_policy.py +0 -0
  123. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_sqs_queue_policy.py +0 -0
  124. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/resources/test_wafv2_ip_set.py +0 -0
  125. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_action_expander.py +0 -0
  126. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_constants.py +0 -0
  127. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_generic.py +0 -0
  128. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_parameter.py +0 -0
  129. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_regexs.py +0 -0
  130. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_resolver.py +0 -0
  131. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_resource.py +0 -0
  132. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_resource_generator.py +0 -0
  133. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_types.py +0 -0
  134. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/tests/test_utils.py +0 -0
  135. {pycfmodel-2.0.0 → pycfmodel-2.0.1}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycfmodel
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: A python model for Cloud Formation scripts
5
5
  Author-email: Skyscanner Security <security@skyscanner.net>
6
6
  Project-URL: Homepage, https://github.com/Skyscanner/pycfmodel
@@ -9473,6 +9473,10 @@ CLOUDFORMATION_ACTIONS = [
9473
9473
  "importexport:ListJobs",
9474
9474
  "importexport:UpdateJob",
9475
9475
  "inspector-scan:ScanSbom",
9476
+ "inspector2-telemetry:NotifyHeartbeat",
9477
+ "inspector2-telemetry:SendTelemetry",
9478
+ "inspector2-telemetry:StartSession",
9479
+ "inspector2-telemetry:StopSession",
9476
9480
  "inspector2:AssociateMember",
9477
9481
  "inspector2:BatchAssociateCodeSecurityScanConfiguration",
9478
9482
  "inspector2:BatchDisassociateCodeSecurityScanConfiguration",
@@ -10601,11 +10605,13 @@ CLOUDFORMATION_ACTIONS = [
10601
10605
  "kafka:CreateClusterV2",
10602
10606
  "kafka:CreateConfiguration",
10603
10607
  "kafka:CreateReplicator",
10608
+ "kafka:CreateTopic",
10604
10609
  "kafka:CreateVpcConnection",
10605
10610
  "kafka:DeleteCluster",
10606
10611
  "kafka:DeleteClusterPolicy",
10607
10612
  "kafka:DeleteConfiguration",
10608
10613
  "kafka:DeleteReplicator",
10614
+ "kafka:DeleteTopic",
10609
10615
  "kafka:DeleteVpcConnection",
10610
10616
  "kafka:DescribeCluster",
10611
10617
  "kafka:DescribeClusterOperation",
@@ -10651,6 +10657,7 @@ CLOUDFORMATION_ACTIONS = [
10651
10657
  "kafka:UpdateReplicationInfo",
10652
10658
  "kafka:UpdateSecurity",
10653
10659
  "kafka:UpdateStorage",
10660
+ "kafka:UpdateTopic",
10654
10661
  "kafkaconnect:CreateConnector",
10655
10662
  "kafkaconnect:CreateCustomPlugin",
10656
10663
  "kafkaconnect:CreateWorkerConfiguration",
@@ -403,6 +403,18 @@ class MixedInstancesPolicy(CustomModel):
403
403
  ResolvableMixedInstancesPolicy = ResolvableModel(MixedInstancesPolicy)
404
404
 
405
405
 
406
+ # Pre-resolved list type to avoid class body name shadowing in AutoScalingAutoScalingGroupProperties.
407
+ # The AutoScalingAutoScalingGroupProperties class has a field named "MetricsCollection" (default None) which shadows
408
+ # the MetricsCollection class when Python evaluates List[MetricsCollection] inside the class body.
409
+ _MetricsCollectionList = Resolvable[List[MetricsCollection]]
410
+
411
+
412
+ # Pre-resolved list type to avoid class body name shadowing in AutoScalingAutoScalingGroupProperties.
413
+ # The AutoScalingAutoScalingGroupProperties class has a field named "NotificationConfiguration" (default None) which shadows
414
+ # the NotificationConfiguration class when Python evaluates List[NotificationConfiguration] inside the class body.
415
+ _NotificationConfigurationList = Resolvable[List[NotificationConfiguration]]
416
+
417
+
406
418
  class AutoScalingAutoScalingGroupProperties(CustomModel):
407
419
  """
408
420
  Properties for AWS::AutoScaling::AutoScalingGroup.
@@ -419,6 +431,7 @@ class AutoScalingAutoScalingGroupProperties(CustomModel):
419
431
  - Cooldown: *Only needed if you use simple scaling policies.*
420
432
  The amount of time, in secon...
421
433
  - DefaultInstanceWarmup: The amount of time, in seconds, until a new instance is considered to have finis...
434
+ - DeletionProtection:
422
435
  - DesiredCapacity: The desired capacity is the initial capacity of the Auto Scaling group at the ti...
423
436
  - DesiredCapacityType: The unit of measurement for the value specified for desired capacity. Amazon EC2...
424
437
  - HealthCheckGracePeriod: The amount of time, in seconds, that Amazon EC2 Auto Scaling waits before checki...
@@ -465,6 +478,7 @@ class AutoScalingAutoScalingGroupProperties(CustomModel):
465
478
  Context: Optional[ResolvableStr] = None
466
479
  Cooldown: Optional[ResolvableStr] = None
467
480
  DefaultInstanceWarmup: Optional[ResolvableInt] = None
481
+ DeletionProtection: Optional[ResolvableStr] = None
468
482
  DesiredCapacity: Optional[ResolvableStr] = None
469
483
  DesiredCapacityType: Optional[ResolvableStr] = None
470
484
  HealthCheckGracePeriod: Optional[ResolvableInt] = None
@@ -477,11 +491,11 @@ class AutoScalingAutoScalingGroupProperties(CustomModel):
477
491
  LifecycleHookSpecificationList: Optional[Resolvable[List[LifecycleHookSpecification]]] = None
478
492
  LoadBalancerNames: Optional[Resolvable[List[ResolvableStr]]] = None
479
493
  MaxInstanceLifetime: Optional[ResolvableInt] = None
480
- MetricsCollection: Optional[Resolvable[List[MetricsCollection]]] = None
494
+ MetricsCollection: Optional[_MetricsCollectionList] = None
481
495
  MixedInstancesPolicy: Optional[ResolvableMixedInstancesPolicy] = None
482
496
  NewInstancesProtectedFromScaleIn: Optional[ResolvableBool] = None
483
497
  NotificationConfiguration: Optional[ResolvableNotificationConfiguration] = None
484
- NotificationConfigurations: Optional[Resolvable[List[NotificationConfiguration]]] = None
498
+ NotificationConfigurations: Optional[_NotificationConfigurationList] = None
485
499
  PlacementGroup: Optional[ResolvableStr] = None
486
500
  ServiceLinkedRoleARN: Optional[ResolvableStr] = None
487
501
  SkipZonalShiftValidation: Optional[ResolvableBool] = None
@@ -7,6 +7,7 @@ Auto-generated from AWS CloudFormation schema for AWS::CloudWatch::Alarm.
7
7
  from typing import List, Literal, Optional
8
8
 
9
9
  from pycfmodel.model.base import CustomModel
10
+ from pycfmodel.model.resources.properties.tag import Tag
10
11
  from pycfmodel.model.resources.resource import Resource
11
12
  from pycfmodel.model.types import Resolvable, ResolvableBool, ResolvableInt, ResolvableModel, ResolvableStr
12
13
 
@@ -67,18 +68,6 @@ class MetricDataQuery(CustomModel):
67
68
  ResolvableMetricDataQuery = ResolvableModel(MetricDataQuery)
68
69
 
69
70
 
70
- class Tag(CustomModel):
71
- """
72
- One of the key-value pairs associated with the alarm.
73
- """
74
-
75
- Key: ResolvableStr
76
- Value: ResolvableStr
77
-
78
-
79
- ResolvableTag = ResolvableModel(Tag)
80
-
81
-
82
71
  class CloudWatchAlarmProperties(CustomModel):
83
72
  """
84
73
  Properties for AWS::CloudWatch::Alarm.
@@ -7,7 +7,7 @@ Auto-generated from AWS CloudFormation schema for AWS::DynamoDB::Table.
7
7
  from typing import List, Literal, Optional
8
8
 
9
9
  from pycfmodel.model.base import CustomModel
10
- from pycfmodel.model.generic import ResolvableGeneric
10
+ from pycfmodel.model.resources.properties.tag import Tag
11
11
  from pycfmodel.model.resources.resource import Resource
12
12
  from pycfmodel.model.types import Resolvable, ResolvableBool, ResolvableInt, ResolvableModel, ResolvableStr
13
13
 
@@ -119,13 +119,19 @@ class Projection(CustomModel):
119
119
  ResolvableProjection = ResolvableModel(Projection)
120
120
 
121
121
 
122
+ # Pre-resolved list type to avoid class body name shadowing in LocalSecondaryIndex.
123
+ # The LocalSecondaryIndex class has a field named "KeySchema" (default None) which shadows
124
+ # the KeySchema class when Python evaluates List[KeySchema] inside the class body.
125
+ _KeySchemaList = Resolvable[List[KeySchema]]
126
+
127
+
122
128
  class LocalSecondaryIndex(CustomModel):
123
129
  """
124
130
  Represents the properties of a local secondary index.
125
131
  """
126
132
 
127
133
  IndexName: ResolvableStr
128
- KeySchema: Resolvable[List[KeySchema]]
134
+ KeySchema: _KeySchemaList
129
135
  Projection: ResolvableProjection
130
136
 
131
137
 
@@ -149,7 +155,7 @@ class ResourcePolicy(CustomModel):
149
155
  Creates or updates a resource-based policy document that contains the permissions for DDB resources, such as a table, its indexes, and stream.
150
156
  """
151
157
 
152
- PolicyDocument: ResolvableGeneric
158
+ PolicyDocument: Resolvable[dict]
153
159
 
154
160
 
155
161
  ResolvableResourcePolicy = ResolvableModel(ResourcePolicy)
@@ -207,18 +213,6 @@ class StreamSpecification(CustomModel):
207
213
  ResolvableStreamSpecification = ResolvableModel(StreamSpecification)
208
214
 
209
215
 
210
- class Tag(CustomModel):
211
- """
212
- Describes a tag.
213
- """
214
-
215
- Key: ResolvableStr
216
- Value: ResolvableStr
217
-
218
-
219
- ResolvableTag = ResolvableModel(Tag)
220
-
221
-
222
216
  class TimeToLiveSpecification(CustomModel):
223
217
  """
224
218
  Represents the settings used to enable or disable Time to Live (TTL) for the specified table.
@@ -243,13 +237,19 @@ class WarmThroughput(CustomModel):
243
237
  ResolvableWarmThroughput = ResolvableModel(WarmThroughput)
244
238
 
245
239
 
240
+ # Pre-resolved list type to avoid class body name shadowing in GlobalSecondaryIndex.
241
+ # The GlobalSecondaryIndex class has a field named "KeySchema" (default None) which shadows
242
+ # the KeySchema class when Python evaluates List[KeySchema] inside the class body.
243
+ _KeySchemaList = Resolvable[List[KeySchema]]
244
+
245
+
246
246
  class GlobalSecondaryIndex(CustomModel):
247
247
  """
248
248
  Represents the properties of a global secondary index.
249
249
  """
250
250
 
251
251
  IndexName: ResolvableStr
252
- KeySchema: Resolvable[List[KeySchema]]
252
+ KeySchema: _KeySchemaList
253
253
  Projection: ResolvableProjection
254
254
  ContributorInsightsSpecification: Optional[ResolvableContributorInsightsSpecification] = None
255
255
  OnDemandThroughput: Optional[ResolvableOnDemandThroughput] = None
@@ -260,6 +260,12 @@ class GlobalSecondaryIndex(CustomModel):
260
260
  ResolvableGlobalSecondaryIndex = ResolvableModel(GlobalSecondaryIndex)
261
261
 
262
262
 
263
+ # Pre-resolved list type to avoid class body name shadowing in DynamoDBTableProperties.
264
+ # The DynamoDBTableProperties class has a field named "KeySchema" (default None) which shadows
265
+ # the KeySchema class when Python evaluates List[KeySchema] inside the class body.
266
+ _KeySchemaList = Resolvable[List[KeySchema]]
267
+
268
+
263
269
  class DynamoDBTableProperties(CustomModel):
264
270
  """
265
271
  Properties for AWS::DynamoDB::Table.
@@ -293,7 +299,7 @@ class DynamoDBTableProperties(CustomModel):
293
299
  More info at [AWS Docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html)
294
300
  """
295
301
 
296
- KeySchema: Resolvable[List[KeySchema]]
302
+ KeySchema: _KeySchemaList
297
303
  AttributeDefinitions: Optional[Resolvable[List[AttributeDefinition]]] = None
298
304
  BillingMode: Optional[ResolvableStr] = None
299
305
  ContributorInsightsSpecification: Optional[ResolvableContributorInsightsSpecification] = None
@@ -7,6 +7,7 @@ Auto-generated from AWS CloudFormation schema for AWS::ElasticLoadBalancingV2::T
7
7
  from typing import List, Literal, Optional
8
8
 
9
9
  from pycfmodel.model.base import CustomModel
10
+ from pycfmodel.model.resources.properties.tag import Tag
10
11
  from pycfmodel.model.resources.resource import Resource
11
12
  from pycfmodel.model.types import Resolvable, ResolvableBool, ResolvableInt, ResolvableModel, ResolvableStr
12
13
 
@@ -23,18 +24,6 @@ class Matcher(CustomModel):
23
24
  ResolvableMatcher = ResolvableModel(Matcher)
24
25
 
25
26
 
26
- class Tag(CustomModel):
27
- """
28
- Tag configuration.
29
- """
30
-
31
- Key: ResolvableStr
32
- Value: ResolvableStr
33
-
34
-
35
- ResolvableTag = ResolvableModel(Tag)
36
-
37
-
38
27
  class TargetDescription(CustomModel):
39
28
  """
40
29
  TargetDescription configuration.
@@ -125,4 +114,4 @@ class ELBv2TargetGroup(Resource):
125
114
  """
126
115
 
127
116
  Type: Literal["AWS::ElasticLoadBalancingV2::TargetGroup"]
128
- Properties: Optional[Resolvable[ELBv2TargetGroupProperties]] = None
117
+ Properties: Resolvable[ELBv2TargetGroupProperties] = ELBv2TargetGroupProperties()
@@ -35,7 +35,7 @@ class IAMGroup(Resource):
35
35
  """
36
36
 
37
37
  Type: Literal["AWS::IAM::Group"]
38
- Properties: Optional[Resolvable[IAMGroupProperties]] = None
38
+ Properties: Resolvable[IAMGroupProperties] = IAMGroupProperties()
39
39
 
40
40
  @property
41
41
  def policy_documents(self) -> List[OptionallyNamedPolicyDocument]:
@@ -59,7 +59,7 @@ class IAMUser(Resource):
59
59
  """
60
60
 
61
61
  Type: Literal["AWS::IAM::User"]
62
- Properties: Optional[Resolvable[IAMUserProperties]] = None
62
+ Properties: Resolvable[IAMUserProperties] = IAMUserProperties()
63
63
 
64
64
  def has_hardcoded_credentials(self) -> bool:
65
65
  """Returns True if login profile password contains a hardcoded string, otherwise False."""
@@ -814,6 +814,18 @@ class Transition(CustomModel):
814
814
  ResolvableTransition = ResolvableModel(Transition)
815
815
 
816
816
 
817
+ # Pre-resolved list type to avoid class body name shadowing in Rule.
818
+ # The Rule class has a field named "NoncurrentVersionTransition" (default None) which shadows
819
+ # the NoncurrentVersionTransition class when Python evaluates List[NoncurrentVersionTransition] inside the class body.
820
+ _NoncurrentVersionTransitionList = Resolvable[List[NoncurrentVersionTransition]]
821
+
822
+
823
+ # Pre-resolved list type to avoid class body name shadowing in Rule.
824
+ # The Rule class has a field named "Transition" (default None) which shadows
825
+ # the Transition class when Python evaluates List[Transition] inside the class body.
826
+ _TransitionList = Resolvable[List[Transition]]
827
+
828
+
817
829
  class Rule(CustomModel):
818
830
  """
819
831
  Specifies lifecycle rules for an Amazon S3 bucket.
@@ -828,13 +840,13 @@ class Rule(CustomModel):
828
840
  NoncurrentVersionExpiration: Optional[ResolvableNoncurrentVersionExpiration] = None
829
841
  NoncurrentVersionExpirationInDays: Optional[ResolvableInt] = None
830
842
  NoncurrentVersionTransition: Optional[ResolvableNoncurrentVersionTransition] = None
831
- NoncurrentVersionTransitions: Optional[Resolvable[List[NoncurrentVersionTransition]]] = None
843
+ NoncurrentVersionTransitions: Optional[_NoncurrentVersionTransitionList] = None
832
844
  ObjectSizeGreaterThan: Optional[ResolvableStr] = None
833
845
  ObjectSizeLessThan: Optional[ResolvableStr] = None
834
846
  Prefix: Optional[ResolvableStr] = None
835
847
  TagFilters: Optional[Resolvable[List[TagFilter]]] = None
836
848
  Transition: Optional[ResolvableTransition] = None
837
- Transitions: Optional[Resolvable[List[Transition]]] = None
849
+ Transitions: Optional[_TransitionList] = None
838
850
 
839
851
 
840
852
  ResolvableRule = ResolvableModel(Rule)
@@ -923,4 +935,4 @@ class S3Bucket(Resource):
923
935
  """
924
936
 
925
937
  Type: Literal["AWS::S3::Bucket"]
926
- Properties: Optional[Resolvable[S3BucketProperties]] = None
938
+ Properties: Resolvable[S3BucketProperties] = S3BucketProperties()
@@ -7,6 +7,7 @@ Auto-generated from AWS CloudFormation schema for AWS::SNS::Topic.
7
7
  from typing import List, Literal, Optional
8
8
 
9
9
  from pycfmodel.model.base import CustomModel
10
+ from pycfmodel.model.resources.properties.tag import Tag
10
11
  from pycfmodel.model.resources.resource import Resource
11
12
  from pycfmodel.model.types import Resolvable, ResolvableBool, ResolvableModel, ResolvableStr
12
13
 
@@ -37,16 +38,10 @@ class Subscription(CustomModel):
37
38
  ResolvableSubscription = ResolvableModel(Subscription)
38
39
 
39
40
 
40
- class Tag(CustomModel):
41
- """
42
- The list of tags to be added to the specified topic.
43
- """
44
-
45
- Key: ResolvableStr
46
- Value: ResolvableStr
47
-
48
-
49
- ResolvableTag = ResolvableModel(Tag)
41
+ # Pre-resolved list type to avoid class body name shadowing in SNSTopicProperties.
42
+ # The SNSTopicProperties class has a field named "Subscription" (default None) which shadows
43
+ # the Subscription class when Python evaluates List[Subscription] inside the class body.
44
+ _SubscriptionList = Resolvable[List[Subscription]]
50
45
 
51
46
 
52
47
  class SNSTopicProperties(CustomModel):
@@ -85,7 +80,7 @@ class SNSTopicProperties(CustomModel):
85
80
  FifoTopic: Optional[ResolvableBool] = None
86
81
  KmsMasterKeyId: Optional[ResolvableStr] = None
87
82
  SignatureVersion: Optional[ResolvableStr] = None
88
- Subscription: Optional[Resolvable[List[Subscription]]] = None
83
+ Subscription: Optional[_SubscriptionList] = None
89
84
  Tags: Optional[Resolvable[List[Tag]]] = None
90
85
  TopicName: Optional[ResolvableStr] = None
91
86
  TracingConfig: Optional[ResolvableStr] = None
@@ -103,4 +98,4 @@ class SNSTopic(Resource):
103
98
  """
104
99
 
105
100
  Type: Literal["AWS::SNS::Topic"]
106
- Properties: Optional[Resolvable[SNSTopicProperties]] = None
101
+ Properties: Resolvable[SNSTopicProperties] = SNSTopicProperties()
@@ -7,18 +7,9 @@ Auto-generated from AWS CloudFormation schema for AWS::SQS::Queue.
7
7
  from typing import List, Literal, Optional
8
8
 
9
9
  from pycfmodel.model.base import CustomModel
10
+ from pycfmodel.model.resources.properties.tag import Tag
10
11
  from pycfmodel.model.resources.resource import Resource
11
- from pycfmodel.model.types import Resolvable, ResolvableBool, ResolvableInt, ResolvableModel, ResolvableStr
12
-
13
-
14
- class Tag(CustomModel):
15
- """ """
16
-
17
- Key: ResolvableStr
18
- Value: ResolvableStr
19
-
20
-
21
- ResolvableTag = ResolvableModel(Tag)
12
+ from pycfmodel.model.types import Resolvable, ResolvableBool, ResolvableInt, ResolvableStr
22
13
 
23
14
 
24
15
  class SQSQueueProperties(CustomModel):
@@ -77,4 +68,4 @@ class SQSQueue(Resource):
77
68
  """
78
69
 
79
70
  Type: Literal["AWS::SQS::Queue"]
80
- Properties: Optional[Resolvable[SQSQueueProperties]] = None
71
+ Properties: Resolvable[SQSQueueProperties] = SQSQueueProperties()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pycfmodel
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: A python model for Cloud Formation scripts
5
5
  Author-email: Skyscanner Security <security@skyscanner.net>
6
6
  Project-URL: Homepage, https://github.com/Skyscanner/pycfmodel
@@ -279,37 +279,64 @@ class CodeGenerator:
279
279
  # Handle anyOf/oneOf
280
280
  if "anyOf" in prop_schema or "oneOf" in prop_schema:
281
281
  variants = prop_schema.get("anyOf") or prop_schema.get("oneOf", [])
282
- # Check if any variant is an array
283
- for variant in variants:
284
- if variant.get("type") == "array":
285
- items = variant.get("items", {})
286
- item_type = self._get_item_type(items)
287
- self.imports.add("from typing import List")
288
- return f"Resolvable[List[{item_type}]]"
289
- # Check for $ref in variants
290
- for variant in variants:
291
- if "$ref" in variant:
292
- ref = variant["$ref"]
293
- if ref.startswith("#/definitions/"):
294
- def_name = ref.split("/")[-1]
295
- definition = self.definitions.get(def_name, {})
296
- if definition.get("type") == "object" or definition.get("properties"):
297
- # Generate a nested model class and use ResolvableModel
298
- self.needed_definitions.add(def_name)
299
- self.imports.add("from pycfmodel.model.types import ResolvableModel")
300
- return f"Resolvable{def_name}"
301
- else:
302
- # Fall back to inline handling
303
- return self.get_type_for_schema(variant, is_required, context)
304
- return "Resolvable[dict]"
282
+
283
+ # Skip variants that are only relationshipRef metadata (not real type info).
284
+ # If all variants are just relationshipRef, fall through to the "type" field.
285
+ typed_variants = [v for v in variants if not (set(v.keys()) <= {"relationshipRef"})]
286
+
287
+ if typed_variants:
288
+ # Check if any variant is an array
289
+ for variant in typed_variants:
290
+ if variant.get("type") == "array":
291
+ items = variant.get("items", {})
292
+ item_type = self._get_item_type(items)
293
+ self.imports.add("from typing import List")
294
+ return f"Resolvable[List[{item_type}]]"
295
+ # Check for $ref in variants
296
+ for variant in typed_variants:
297
+ if "$ref" in variant:
298
+ ref = variant["$ref"]
299
+ if ref.startswith("#/definitions/"):
300
+ def_name = ref.split("/")[-1]
301
+ definition = self.definitions.get(def_name, {})
302
+ if definition.get("type") == "object" or definition.get("properties"):
303
+ # Generate a nested model class and use ResolvableModel
304
+ self.needed_definitions.add(def_name)
305
+ self.imports.add("from pycfmodel.model.types import ResolvableModel")
306
+ return f"Resolvable{def_name}"
307
+ else:
308
+ # Fall back to inline handling
309
+ return self.get_type_for_schema(variant, is_required, context)
310
+ return "Resolvable[dict]"
311
+ # All variants were relationshipRef only — fall through to use the "type" field
305
312
 
306
313
  json_type = prop_schema.get("type")
307
314
 
308
315
  if json_type is None:
309
316
  return "Resolvable[dict]"
310
317
 
311
- # Handle multiple types (e.g., ["string", "object"])
318
+ # Handle multiple types (e.g., ["string", "object"]).
319
+ # If "object" is present, use Resolvable[dict] since it accepts both dicts and strings.
320
+ # Otherwise, use the first recognized primitive type.
312
321
  if isinstance(json_type, list):
322
+ if "object" in json_type:
323
+ return "Resolvable[dict]"
324
+ type_map = {
325
+ "string": "ResolvableStr",
326
+ "integer": "ResolvableInt",
327
+ "number": "ResolvableInt",
328
+ "boolean": "ResolvableBool",
329
+ }
330
+ for t in json_type:
331
+ if t in type_map:
332
+ type_str = type_map[t]
333
+ if type_str == "ResolvableStr":
334
+ self.imports.add("from pycfmodel.model.types import ResolvableStr")
335
+ elif type_str == "ResolvableInt":
336
+ self.imports.add("from pycfmodel.model.types import ResolvableInt")
337
+ elif type_str == "ResolvableBool":
338
+ self.imports.add("from pycfmodel.model.types import ResolvableBool")
339
+ return type_str
313
340
  return "Resolvable[dict]"
314
341
 
315
342
  # Handle array types
@@ -464,6 +491,59 @@ class CodeGenerator:
464
491
  return True
465
492
  return False
466
493
 
494
+ def _detect_field_name_shadowing(self, fields: List[Tuple[str, str, str, bool]]) -> Dict[str, str]:
495
+ """
496
+ Detect fields whose names shadow class names used in List[] annotations of sibling fields.
497
+
498
+ When a class has a field like `Transition: Optional[...] = None` and another field
499
+ `Transitions: Optional[Resolvable[List[Transition]]] = None`, Python evaluates the
500
+ List[Transition] annotation inside the class body where `Transition` has already been
501
+ set to None (the default value), causing List[Transition] to resolve to List[NoneType].
502
+
503
+ Returns a dict mapping class names to their pre-resolved type alias names.
504
+ """
505
+ field_names = {name for name, _, _, _ in fields}
506
+ aliases = {}
507
+
508
+ for _, type_annotation, _, _ in fields:
509
+ # Find all List[ClassName] patterns in the annotation
510
+ match = re.search(r"Resolvable\[List\[(\w+)\]\]", type_annotation)
511
+ if match:
512
+ class_name = match.group(1)
513
+ if class_name in field_names and class_name not in aliases:
514
+ alias_name = f"_{class_name}List"
515
+ aliases[class_name] = alias_name
516
+
517
+ return aliases
518
+
519
+ def _apply_shadowing_aliases(
520
+ self, fields: List[Tuple[str, str, str, bool]], aliases: Dict[str, str]
521
+ ) -> List[Tuple[str, str, str, bool]]:
522
+ """Replace Resolvable[List[ClassName]] with the pre-resolved alias in field annotations."""
523
+ if not aliases:
524
+ return fields
525
+
526
+ result = []
527
+ for name, type_annotation, default, is_required in fields:
528
+ for class_name, alias_name in aliases.items():
529
+ type_annotation = type_annotation.replace(f"Resolvable[List[{class_name}]]", alias_name)
530
+ result.append((name, type_annotation, default, is_required))
531
+ return result
532
+
533
+ def _generate_shadowing_alias_lines(self, aliases: Dict[str, str], class_name: str) -> List[str]:
534
+ """Generate module-level type alias lines to avoid class body name shadowing."""
535
+ lines = []
536
+ for original_class, alias_name in sorted(aliases.items()):
537
+ lines.append(f"# Pre-resolved list type to avoid class body name shadowing in {class_name}.")
538
+ lines.append(f'# The {class_name} class has a field named "{original_class}" (default None) which shadows')
539
+ lines.append(
540
+ f"# the {original_class} class when Python evaluates List[{original_class}] inside the class body."
541
+ )
542
+ lines.append(f"{alias_name} = Resolvable[List[{original_class}]]")
543
+ lines.append("")
544
+ lines.append("")
545
+ return lines
546
+
467
547
  def generate_definition_class(self, def_name: str) -> List[str]:
468
548
  """Generate a class for a schema definition."""
469
549
  if def_name in self.generated_definitions:
@@ -482,15 +562,6 @@ class CodeGenerator:
482
562
  required = set(definition.get("required", []))
483
563
  desc = definition.get("description", f"{def_name} configuration.")
484
564
 
485
- lines = []
486
- lines.append(f"class {def_name}(CustomModel):")
487
- lines.append(' """')
488
- # Truncate description to first sentence or 200 chars
489
- short_desc = desc.split(".")[0] + "." if "." in desc else desc[:200]
490
- lines.append(f" {short_desc}")
491
- lines.append(' """')
492
- lines.append("")
493
-
494
565
  # Generate fields
495
566
  fields = []
496
567
  for prop_name, prop_schema in sorted(props.items()):
@@ -504,6 +575,23 @@ class CodeGenerator:
504
575
  default = "" if is_required else " = None"
505
576
  fields.append((prop_name, type_annotation, default, is_required))
506
577
 
578
+ # Detect and fix field name shadowing in List[] annotations
579
+ aliases = self._detect_field_name_shadowing(fields)
580
+ fields = self._apply_shadowing_aliases(fields, aliases)
581
+
582
+ lines = []
583
+
584
+ # Emit module-level type aliases before the class to avoid shadowing
585
+ lines.extend(self._generate_shadowing_alias_lines(aliases, def_name))
586
+
587
+ lines.append(f"class {def_name}(CustomModel):")
588
+ lines.append(' """')
589
+ # Truncate description to first sentence or 200 chars
590
+ short_desc = desc.split(".")[0] + "." if "." in desc else desc[:200]
591
+ lines.append(f" {short_desc}")
592
+ lines.append(' """')
593
+ lines.append("")
594
+
507
595
  # Sort: required first, then optional
508
596
  required_fields = [(n, t, d) for n, t, d, r in fields if r]
509
597
  optional_fields = [(n, t, d) for n, t, d, r in fields if not r]
@@ -591,6 +679,19 @@ class CodeGenerator:
591
679
  # Each class is immediately followed by its ResolvableX type alias
592
680
  lines.extend(definition_code_lines)
593
681
 
682
+ # Detect and fix field name shadowing in Properties class List[] annotations
683
+ props_fields_for_shadowing = [(n, t, d, r) for n, t, d, _, r in property_fields]
684
+ props_aliases = self._detect_field_name_shadowing(props_fields_for_shadowing)
685
+ if props_aliases:
686
+ property_fields = [
687
+ (n, t_new, d, desc, r)
688
+ for (n, _, d, desc, r), (_, t_new, _, _) in zip(
689
+ property_fields,
690
+ self._apply_shadowing_aliases(props_fields_for_shadowing, props_aliases),
691
+ )
692
+ ]
693
+ lines.extend(self._generate_shadowing_alias_lines(props_aliases, self.props_class_name))
694
+
594
695
  # Properties class
595
696
  lines.append(f"class {self.props_class_name}(CustomModel):")
596
697
  lines.append(' """')
@@ -641,12 +742,13 @@ class CodeGenerator:
641
742
  lines.append("")
642
743
  lines.append(f' Type: Literal["{self.resource_type}"]')
643
744
 
644
- # Determine if Properties should be Optional
745
+ # When no properties are required, default to an empty properties object
746
+ # so that accessing Properties.SomeField returns None instead of raising AttributeError.
645
747
  has_required_props = any(r for _, _, _, _, r in property_fields)
646
748
  if has_required_props:
647
749
  lines.append(f" Properties: Resolvable[{self.props_class_name}]")
648
750
  else:
649
- lines.append(f" Properties: Optional[Resolvable[{self.props_class_name}]] = None")
751
+ lines.append(f" Properties: Resolvable[{self.props_class_name}] = {self.props_class_name}()")
650
752
 
651
753
  lines.append("")
652
754
 
@@ -174,6 +174,41 @@ def test_autoscaling_group_with_notification():
174
174
  assert asg.Properties.NotificationConfiguration.TopicARN == "arn:aws:sns:us-east-1:123456789012:my-topic"
175
175
 
176
176
 
177
+ def test_autoscaling_group_with_notification_configurations():
178
+ """
179
+ Regression test: NotificationConfigurations must be parsed correctly as
180
+ List[NotificationConfiguration], not as List[NoneType] due to field name
181
+ shadowing from the singular NotificationConfiguration field in the class body.
182
+ """
183
+ asg = AutoScalingAutoScalingGroup(
184
+ **{
185
+ "Type": "AWS::AutoScaling::AutoScalingGroup",
186
+ "Properties": {
187
+ "MinSize": "1",
188
+ "MaxSize": "5",
189
+ "LaunchTemplate": {
190
+ "LaunchTemplateId": "lt-123",
191
+ "Version": "$Latest",
192
+ },
193
+ "VPCZoneIdentifier": ["subnet-123"],
194
+ "NotificationConfigurations": [
195
+ {
196
+ "TopicARN": "arn:aws:sns:us-east-1:123456789012:launch-topic",
197
+ "NotificationTypes": ["autoscaling:EC2_INSTANCE_LAUNCH"],
198
+ },
199
+ {
200
+ "TopicARN": "arn:aws:sns:us-east-1:123456789012:terminate-topic",
201
+ "NotificationTypes": ["autoscaling:EC2_INSTANCE_TERMINATE"],
202
+ },
203
+ ],
204
+ },
205
+ }
206
+ )
207
+ assert len(asg.Properties.NotificationConfigurations) == 2
208
+ assert asg.Properties.NotificationConfigurations[0].TopicARN == "arn:aws:sns:us-east-1:123456789012:launch-topic"
209
+ assert asg.Properties.NotificationConfigurations[1].TopicARN == "arn:aws:sns:us-east-1:123456789012:terminate-topic"
210
+
211
+
177
212
  def test_autoscaling_group_with_tags():
178
213
  asg = AutoScalingAutoScalingGroup(
179
214
  **{