Repository: cloudtools/troposphere (python)

Issue:
Incorrect ValueError: AllowedPattern can only be used with parameters of the String type
Example:

```python
    cft.add_parameter(
        Parameter(
            "Foo",
            Type="CommaDelimitedList",
            AllowedPattern="^[A-Z]{2}$",
            Default="",
        )
    )
```

According to the [AWS docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html) this should be allowed:
> AllowedPattern
> A regular expression that represents the patterns to allow for String or CommaDelimitedList types. When applied on a parameter of type String, the pattern must match the entire parameter value provided. When applied to a parameter of type CommaDelimitedList, the pattern must match each value in the list.


Pull request description:
Fix Parameters.validate incorrect validate for CommaDelimitedList
fix #2180

Patch:
diff --git a/pyproject.toml b/pyproject.toml
index bf101701..e57a18df 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -3,7 +3,7 @@ include = [
   "**/troposphere",
 ]
 pythonPlatform = "All"
-pythonVersion = "3.7"
+pythonVersion = "3.8"
 reportDuplicateImport = "none"
 reportImportCycles = "none"
 reportIncompatibleMethodOverride = "warning"
diff --git a/scripts/gen.py b/scripts/gen.py
index 6f202379..d22d26d2 100755
--- a/scripts/gen.py
+++ b/scripts/gen.py
@@ -186,7 +186,6 @@ class ResourceSpec:
                         path = p["path"]
                         obj = jsonpointer.JsonPointer(path).to_last(self.spec)
                         print(f"path: {path}", file=sys.stderr)
-                        print(f"obj: {obj}", file=sys.stderr)
                         raise
 
     def parse(self, limit_warnings=None):
@@ -294,7 +293,7 @@ class ResourceSpec:
         UNSUPPORTED_TAG_NAMES = ["TagMap"]
 
         for service_name, service in self.services.items():
-            for class_name, key, value in service.get_property_items():
+            for class_name, key, value in [x for x in service.get_property_items()]:
                 if key in service.property_validators:
                     continue
                 if value.item_type in UNSUPPORTED_TAG_NAMES:
diff --git a/scripts/patches/events.py b/scripts/patches/events.py
index 4e11fdb2..57024e2e 100644
--- a/scripts/patches/events.py
+++ b/scripts/patches/events.py
@@ -13,9 +13,4 @@ patches = [
             "Required": False,
         },
     },
-    # Remove unused TagEntry
-    {
-        "op": "remove",
-        "path": "/PropertyTypes/AWS::Events::EventBus.TagEntry",
-    },
 ]
diff --git a/setup.cfg b/setup.cfg
index 5c3baf3c..aa9ecb2d 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -16,11 +16,11 @@ classifiers =
     Programming Language :: Python
     Programming Language :: Python :: 3 :: Only
     Programming Language :: Python :: 3
-    Programming Language :: Python :: 3.7
     Programming Language :: Python :: 3.8
     Programming Language :: Python :: 3.9
     Programming Language :: Python :: 3.10
     Programming Language :: Python :: 3.11
+    Programming Language :: Python :: 3.12
 project_urls =
     Changelog = https://github.com/cloudtools/troposphere/blob/master/CHANGELOG.md
     Source = https://github.com/cloudtools/troposphere
@@ -32,7 +32,6 @@ tests_require = awacs>=2.0.0
 include_package_data = true
 install_requires =
     cfn_flip >= 1.0.2
-    typing-extensions >= 3.7.4.3; python_version < "3.8"
 packages =
     troposphere
     troposphere.openstack
@@ -42,7 +41,7 @@ packages =
 scripts =
     scripts/cfn
     scripts/cfn2py
-python_requires = >=3.7.0
+python_requires = >=3.8
 zip_safe = false
 
 [options.extras_require]
diff --git a/troposphere/__init__.py b/troposphere/__init__.py
index 05b1592c..db483c11 100644
--- a/troposphere/__init__.py
+++ b/troposphere/__init__.py
@@ -1011,6 +1011,7 @@ class Output(AWSDeclaration):
 class Parameter(AWSDeclaration):
     STRING_PROPERTIES = ["AllowedPattern", "MaxLength", "MinLength"]
     NUMBER_PROPERTIES = ["MaxValue", "MinValue"]
+    COMMA_DELIMITED_LIST = ["AllowedPattern"]
     props = {
         "Type": (str, True),
         "Default": ((str, int, float), False),
@@ -1076,15 +1077,28 @@ class Parameter(AWSDeclaration):
                     if not any(check_type(x, d) for x in allowed):
                         raise ValueError(error_str % (param_type, type(d), dlist))
 
-        if self.properties["Type"] != "String":
-            for p in self.STRING_PROPERTIES:
+        if self.properties["Type"] == "String":
+            not_allowed = [
+                p for p in self.COMMA_DELIMITED_LIST if p not in self.STRING_PROPERTIES
+            ] + self.NUMBER_PROPERTIES
+            for p in not_allowed:
                 if p in self.properties:
                     raise ValueError(
                         "%s can only be used with parameters of " "the String type." % p
                     )
-        if self.properties["Type"] != "Number":
-            for p in self.NUMBER_PROPERTIES:
+        if self.properties["Type"] == "Number":
+            for p in list(set(self.STRING_PROPERTIES + self.COMMA_DELIMITED_LIST)):
                 if p in self.properties:
                     raise ValueError(
                         "%s can only be used with parameters of " "the Number type." % p
                     )
+        if self.properties["Type"] == "CommaDelimitedList":
+            not_allowed = [
+                p for p in self.STRING_PROPERTIES if p not in self.COMMA_DELIMITED_LIST
+            ] + self.NUMBER_PROPERTIES
+            for p in not_allowed:
+                if p in self.properties:
+                    raise ValueError(
+                        "%s can only be used with parameters of "
+                        "the CommaDelimitedList type." % p
+                    )
diff --git a/troposphere/awslambda.py b/troposphere/awslambda.py
index 049ac930..eb8b3eac 100644
--- a/troposphere/awslambda.py
+++ b/troposphere/awslambda.py
@@ -388,6 +388,7 @@ class VPCConfig(AWSProperty):
     """
 
     props: PropsDictType = {
+        "Ipv6AllowedForDualStack": (boolean, False),
         "SecurityGroupIds": ([str], False),
         "SubnetIds": ([str], False),
     }
diff --git a/troposphere/cloudfront.py b/troposphere/cloudfront.py
index 3a94736d..c36399f4 100644
--- a/troposphere/cloudfront.py
+++ b/troposphere/cloudfront.py
@@ -130,9 +130,9 @@ class CloudFrontOriginAccessIdentity(AWSObject):
     }
 
 
-class SingleHeaderConfig(AWSProperty):
+class SingleHeaderPolicyConfig(AWSProperty):
     """
-    `SingleHeaderConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-continuousdeploymentpolicy-singleheaderconfig.html>`__
+    `SingleHeaderPolicyConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-continuousdeploymentpolicy-singleheaderpolicyconfig.html>`__
     """
 
     props: PropsDictType = {
@@ -152,6 +152,28 @@ class SessionStickinessConfig(AWSProperty):
     }
 
 
+class SingleWeightPolicyConfig(AWSProperty):
+    """
+    `SingleWeightPolicyConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-continuousdeploymentpolicy-singleweightpolicyconfig.html>`__
+    """
+
+    props: PropsDictType = {
+        "SessionStickinessConfig": (SessionStickinessConfig, False),
+        "Weight": (double, True),
+    }
+
+
+class SingleHeaderConfig(AWSProperty):
+    """
+    `SingleHeaderConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-continuousdeploymentpolicy-singleheaderconfig.html>`__
+    """
+
+    props: PropsDictType = {
+        "Header": (str, True),
+        "Value": (str, True),
+    }
+
+
 class SingleWeightConfig(AWSProperty):
     """
     `SingleWeightConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-continuousdeploymentpolicy-singleweightconfig.html>`__
@@ -182,8 +204,11 @@ class ContinuousDeploymentPolicyConfig(AWSProperty):
 
     props: PropsDictType = {
         "Enabled": (boolean, True),
+        "SingleHeaderPolicyConfig": (SingleHeaderPolicyConfig, False),
+        "SingleWeightPolicyConfig": (SingleWeightPolicyConfig, False),
         "StagingDistributionDnsNames": ([str], True),
         "TrafficConfig": (TrafficConfig, False),
+        "Type": (str, False),
     }
 
 
diff --git a/troposphere/connectcampaigns.py b/troposphere/connectcampaigns.py
index efd98020..c4c61927 100644
--- a/troposphere/connectcampaigns.py
+++ b/troposphere/connectcampaigns.py
@@ -10,6 +10,16 @@ from . import AWSObject, AWSProperty, PropsDictType, Tags
 from .validators import boolean, double
 
 
+class AgentlessDialerConfig(AWSProperty):
+    """
+    `AgentlessDialerConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connectcampaigns-campaign-agentlessdialerconfig.html>`__
+    """
+
+    props: PropsDictType = {
+        "DialingCapacity": (double, False),
+    }
+
+
 class PredictiveDialerConfig(AWSProperty):
     """
     `PredictiveDialerConfig <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-connectcampaigns-campaign-predictivedialerconfig.html>`__
@@ -17,6 +27,7 @@ class PredictiveDialerConfig(AWSProperty):
 
     props: PropsDictType = {
         "BandwidthAllocation": (double, True),
+        "DialingCapacity": (double, False),
     }
 
 
@@ -27,6 +38,7 @@ class ProgressiveDialerConfig(AWSProperty):
 
     props: PropsDictType = {
         "BandwidthAllocation": (double, True),
+        "DialingCapacity": (double, False),
     }
 
 
@@ -36,6 +48,7 @@ class DialerConfig(AWSProperty):
     """
 
     props: PropsDictType = {
+        "AgentlessDialerConfig": (AgentlessDialerConfig, False),
         "PredictiveDialerConfig": (PredictiveDialerConfig, False),
         "ProgressiveDialerConfig": (ProgressiveDialerConfig, False),
     }
@@ -59,7 +72,7 @@ class OutboundCallConfig(AWSProperty):
     props: PropsDictType = {
         "AnswerMachineDetectionConfig": (AnswerMachineDetectionConfig, False),
         "ConnectContactFlowArn": (str, True),
-        "ConnectQueueArn": (str, True),
+        "ConnectQueueArn": (str, False),
         "ConnectSourcePhoneNumber": (str, False),
     }
 
diff --git a/troposphere/events.py b/troposphere/events.py
index 8fb585ac..1e8bdf22 100644
--- a/troposphere/events.py
+++ b/troposphere/events.py
@@ -426,8 +426,7 @@ class RedshiftDataParameters(AWSProperty):
         "Database": (str, True),
         "DbUser": (str, False),
         "SecretManagerArn": (str, False),
-        "Sql": (str, True),
-        "Sqls": ([str], False),
+        "Sql": (str, False),
         "StatementName": (str, False),
         "WithEvent": (boolean, False),
     }
@@ -539,3 +538,14 @@ class Rule(AWSObject):
         "Tags": (Tags, False),
         "Targets": ([Target], False),
     }
+
+
+class TagEntry(AWSProperty):
+    """
+    `TagEntry <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-events-eventbus-tagentry.html>`__
+    """
+
+    props: PropsDictType = {
+        "Key": (str, True),
+        "Value": (str, True),
+    }
diff --git a/troposphere/iot.py b/troposphere/iot.py
index 62b8fd83..4074ef31 100644
--- a/troposphere/iot.py
+++ b/troposphere/iot.py
@@ -411,6 +411,7 @@ class JobTemplate(AWSObject):
     props: PropsDictType = {
         "AbortConfig": (validate_json_checker, False),
         "Description": (str, True),
+        "DestinationPackageVersions": ([str], False),
         "Document": (str, False),
         "DocumentSource": (str, False),
         "JobArn": (str, False),
@@ -748,6 +749,36 @@ class SecurityProfile(AWSObject):
     }
 
 
+class SoftwarePackage(AWSObject):
+    """
+    `SoftwarePackage <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-softwarepackage.html>`__
+    """
+
+    resource_type = "AWS::IoT::SoftwarePackage"
+
+    props: PropsDictType = {
+        "Description": (str, False),
+        "PackageName": (str, False),
+        "Tags": (Tags, False),
+    }
+
+
+class SoftwarePackageVersion(AWSObject):
+    """
+    `SoftwarePackageVersion <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-softwarepackageversion.html>`__
+    """
+
+    resource_type = "AWS::IoT::SoftwarePackageVersion"
+
+    props: PropsDictType = {
+        "Attributes": (dict, False),
+        "Description": (str, False),
+        "PackageName": (str, True),
+        "Tags": (Tags, False),
+        "VersionName": (str, False),
+    }
+
+
 class AttributePayload(AWSProperty):
     """
     `AttributePayload <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-thinggroup-attributepayload.html>`__
diff --git a/troposphere/mediatailor.py b/troposphere/mediatailor.py
index 963b9f42..ec227f39 100644
--- a/troposphere/mediatailor.py
+++ b/troposphere/mediatailor.py
@@ -39,6 +39,7 @@ class HlsPlaylistSettings(AWSProperty):
     """
 
     props: PropsDictType = {
+        "AdMarkupType": ([str], False),
         "ManifestWindowSeconds": (double, False),
     }
 
diff --git a/troposphere/networkmanager.py b/troposphere/networkmanager.py
index 262c74dc..8a4f88dc 100644
--- a/troposphere/networkmanager.py
+++ b/troposphere/networkmanager.py
@@ -158,7 +158,9 @@ class GlobalNetwork(AWSObject):
     resource_type = "AWS::NetworkManager::GlobalNetwork"
 
     props: PropsDictType = {
+        "CreatedAt": (str, False),
         "Description": (str, False),
+        "State": (str, False),
         "Tags": (Tags, False),
     }
 
diff --git a/troposphere/quicksight.py b/troposphere/quicksight.py
index a89ce7d2..986fef71 100644
--- a/troposphere/quicksight.py
+++ b/troposphere/quicksight.py
@@ -3889,6 +3889,28 @@ class InsightVisual(AWSProperty):
     }
 
 
+class KPIActualValueConditionalFormatting(AWSProperty):
+    """
+    `KPIActualValueConditionalFormatting <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-kpiactualvalueconditionalformatting.html>`__
+    """
+
+    props: PropsDictType = {
+        "Icon": (ConditionalFormattingIcon, False),
+        "TextColor": (ConditionalFormattingColor, False),
+    }
+
+
+class KPIComparisonValueConditionalFormatting(AWSProperty):
+    """
+    `KPIComparisonValueConditionalFormatting <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-kpicomparisonvalueconditionalformatting.html>`__
+    """
+
+    props: PropsDictType = {
+        "Icon": (ConditionalFormattingIcon, False),
+        "TextColor": (ConditionalFormattingColor, False),
+    }
+
+
 class KPIPrimaryValueConditionalFormatting(AWSProperty):
     """
     `KPIPrimaryValueConditionalFormatting <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-kpiprimaryvalueconditionalformatting.html>`__
@@ -3916,6 +3938,8 @@ class KPIConditionalFormattingOption(AWSProperty):
     """
 
     props: PropsDictType = {
+        "ActualValue": (KPIActualValueConditionalFormatting, False),
+        "ComparisonValue": (KPIComparisonValueConditionalFormatting, False),
         "PrimaryValue": (KPIPrimaryValueConditionalFormatting, False),
         "ProgressBar": (KPIProgressBarConditionalFormatting, False),
     }
@@ -3943,6 +3967,39 @@ class KPIFieldWells(AWSProperty):
     }
 
 
+class KPISparklineOptions(AWSProperty):
+    """
+    `KPISparklineOptions <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-kpisparklineoptions.html>`__
+    """
+
+    props: PropsDictType = {
+        "Color": (str, False),
+        "TooltipVisibility": (str, False),
+        "Type": (str, True),
+        "Visibility": (str, False),
+    }
+
+
+class KPIVisualStandardLayout(AWSProperty):
+    """
+    `KPIVisualStandardLayout <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-kpivisualstandardlayout.html>`__
+    """
+
+    props: PropsDictType = {
+        "Type": (str, True),
+    }
+
+
+class KPIVisualLayoutOptions(AWSProperty):
+    """
+    `KPIVisualLayoutOptions <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-kpivisuallayoutoptions.html>`__
+    """
+
+    props: PropsDictType = {
+        "StandardLayout": (KPIVisualStandardLayout, False),
+    }
+
+
 class ProgressBarOptions(AWSProperty):
     """
     `ProgressBarOptions <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-progressbaroptions.html>`__
@@ -3985,7 +4042,9 @@ class KPIOptions(AWSProperty):
         "ProgressBar": (ProgressBarOptions, False),
         "SecondaryValue": (SecondaryValueOptions, False),
         "SecondaryValueFontConfiguration": (FontConfiguration, False),
+        "Sparkline": (KPISparklineOptions, False),
         "TrendArrows": (TrendArrowOptions, False),
+        "VisualLayoutOptions": (KPIVisualLayoutOptions, False),
     }
 
 
@@ -5122,6 +5181,16 @@ class TableFieldOption(AWSProperty):
     }
 
 
+class TablePinnedFieldOptions(AWSProperty):
+    """
+    `TablePinnedFieldOptions <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-tablepinnedfieldoptions.html>`__
+    """
+
+    props: PropsDictType = {
+        "PinnedLeftFields": ([str], False),
+    }
+
+
 class TableFieldOptions(AWSProperty):
     """
     `TableFieldOptions <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-quicksight-template-tablefieldoptions.html>`__
@@ -5129,6 +5198,7 @@ class TableFieldOptions(AWSProperty):
 
     props: PropsDictType = {
         "Order": ([str], False),
+        "PinnedFieldOptions": (TablePinnedFieldOptions, False),
         "SelectedFieldOptions": ([TableFieldOption], False),
     }
 
diff --git a/troposphere/rds.py b/troposphere/rds.py
index 7b09853f..e85442d5 100644
--- a/troposphere/rds.py
+++ b/troposphere/rds.py
@@ -231,6 +231,7 @@ class DBInstance(AWSObject):
         "AllowMajorVersionUpgrade": (boolean, False),
         "AssociatedRoles": ([DBInstanceRole], False),
         "AutoMinorVersionUpgrade": (boolean, False),
+        "AutomaticBackupReplicationRegion": (str, False),
         "AvailabilityZone": (str, False),
         "BackupRetentionPeriod": (validate_backup_retention_period, False),
         "CACertificateIdentifier": (str, False),
diff --git a/troposphere/s3objectlambda.py b/troposphere/s3objectlambda.py
index 65dbf765..5a3776a8 100644
--- a/troposphere/s3objectlambda.py
+++ b/troposphere/s3objectlambda.py
@@ -87,21 +87,11 @@ class Alias(AWSProperty):
     """
 
     props: PropsDictType = {
-        "Status": (str, True),
+        "Status": (str, False),
         "Value": (str, True),
     }
 
 
-class PolicyStatus(AWSProperty):
-    """
-    `PolicyStatus <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3objectlambda-accesspoint-policystatus.html>`__
-    """
-
-    props: PropsDictType = {
-        "IsPublic": (boolean, False),
-    }
-
-
 class PublicAccessBlockConfiguration(AWSProperty):
     """
     `PublicAccessBlockConfiguration <http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3objectlambda-accesspoint-publicaccessblockconfiguration.html>`__
diff --git a/troposphere/type_defs/compat.py b/troposphere/type_defs/compat.py
index dbc27f20..efffe66f 100644
--- a/troposphere/type_defs/compat.py
+++ b/troposphere/type_defs/compat.py
@@ -2,9 +2,4 @@
 # flake8: noqa
 from __future__ import annotations
 
-import sys
-
-if sys.version_info < (3, 8):  # 3.7
-    from typing_extensions import Final, Literal, Protocol, SupportsIndex
-else:
-    from typing import Final, Literal, Protocol, SupportsIndex
+from typing import Final, Literal, Protocol, SupportsIndex


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index c035f835..7fe22cdc 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -15,7 +15,7 @@ jobs:
     name: Python Linting & Tests
     strategy:
       matrix:
-        python-version: ["3.8", "3.9", "3.10", "3.11"]
+        python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"]
         os: [ubuntu-latest]
     runs-on: ${{ matrix.os }}
     steps:
diff --git a/tests/test_parameters.py b/tests/test_parameters.py
index 1c3d98cf..dc28b20f 100644
--- a/tests/test_parameters.py
+++ b/tests/test_parameters.py
@@ -17,5 +17,33 @@ class TestInitArguments(unittest.TestCase):
         self.assertDictEqual(reference.data, {"Ref": "title"})
 
 
+class TestParameterValidator(unittest.TestCase):
+    def test_allowed_pattern_for_number(self):
+        with self.assertRaises(ValueError):
+            Parameter("Foo", Type="Number", AllowedPattern="^[a-zA-Z0-9]*$").validate()
+
+    def test_allowed_pattern_for_comma_delimited_list_and_string(self):
+        Parameter(
+            "Foo",
+            Type="CommaDelimitedList",
+            AllowedPattern="^[A-Z]{2}$",
+            Default="",
+        ).validate()
+
+        Parameter(
+            "Foo",
+            Type="String",
+            AllowedPattern="^[A-Z]{2}$",
+            Default="",
+        ).validate()
+
+    def test_aws_specific_type(self):
+        Parameter(
+            "Foo",
+            Type="AWS::EC2::KeyPair::KeyName",
+            Default="",
+        ).validate()
+
+
 if __name__ == "__main__":
     unittest.main()