localstack-core 4.7.1.dev49__py3-none-any.whl → 4.10.1.dev12__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 (253) hide show
  1. localstack/aws/api/cloudformation/__init__.py +18 -4
  2. localstack/aws/api/cloudwatch/__init__.py +41 -1
  3. localstack/aws/api/config/__init__.py +4 -0
  4. localstack/aws/api/core.py +6 -2
  5. localstack/aws/api/dynamodb/__init__.py +30 -0
  6. localstack/aws/api/ec2/__init__.py +1522 -65
  7. localstack/aws/api/iam/__init__.py +7 -0
  8. localstack/aws/api/kinesis/__init__.py +19 -0
  9. localstack/aws/api/kms/__init__.py +6 -0
  10. localstack/aws/api/lambda_/__init__.py +13 -0
  11. localstack/aws/api/logs/__init__.py +15 -0
  12. localstack/aws/api/redshift/__init__.py +9 -3
  13. localstack/aws/api/route53/__init__.py +5 -0
  14. localstack/aws/api/s3/__init__.py +12 -0
  15. localstack/aws/api/s3control/__init__.py +54 -0
  16. localstack/aws/api/ssm/__init__.py +2 -0
  17. localstack/aws/api/transcribe/__init__.py +17 -0
  18. localstack/aws/client.py +7 -2
  19. localstack/aws/forwarder.py +52 -5
  20. localstack/aws/handlers/analytics.py +1 -1
  21. localstack/aws/handlers/internal_requests.py +6 -1
  22. localstack/aws/handlers/logging.py +12 -2
  23. localstack/aws/handlers/metric_handler.py +41 -1
  24. localstack/aws/handlers/service.py +40 -20
  25. localstack/aws/mocking.py +2 -2
  26. localstack/aws/patches.py +2 -2
  27. localstack/aws/protocol/parser.py +459 -32
  28. localstack/aws/protocol/serializer.py +689 -69
  29. localstack/aws/protocol/service_router.py +120 -20
  30. localstack/aws/protocol/validate.py +1 -1
  31. localstack/aws/scaffold.py +1 -1
  32. localstack/aws/skeleton.py +4 -2
  33. localstack/aws/spec-patches.json +58 -0
  34. localstack/aws/spec.py +37 -16
  35. localstack/cli/exceptions.py +1 -1
  36. localstack/cli/localstack.py +6 -6
  37. localstack/cli/lpm.py +3 -4
  38. localstack/cli/plugins.py +1 -1
  39. localstack/cli/profiles.py +1 -2
  40. localstack/config.py +25 -18
  41. localstack/constants.py +4 -29
  42. localstack/dev/kubernetes/__main__.py +130 -7
  43. localstack/dev/run/configurators.py +1 -4
  44. localstack/dev/run/paths.py +1 -1
  45. localstack/dns/plugins.py +5 -1
  46. localstack/dns/server.py +13 -4
  47. localstack/logging/format.py +3 -3
  48. localstack/packages/api.py +9 -8
  49. localstack/packages/core.py +2 -2
  50. localstack/packages/plugins.py +0 -8
  51. localstack/runtime/analytics.py +3 -0
  52. localstack/runtime/hooks.py +1 -1
  53. localstack/runtime/init.py +2 -2
  54. localstack/runtime/main.py +5 -5
  55. localstack/runtime/patches.py +2 -2
  56. localstack/services/apigateway/helpers.py +1 -4
  57. localstack/services/apigateway/legacy/helpers.py +7 -8
  58. localstack/services/apigateway/legacy/integration.py +4 -3
  59. localstack/services/apigateway/legacy/invocations.py +6 -5
  60. localstack/services/apigateway/legacy/provider.py +148 -68
  61. localstack/services/apigateway/legacy/templates.py +1 -1
  62. localstack/services/apigateway/next_gen/execute_api/handlers/method_request.py +7 -2
  63. localstack/services/apigateway/next_gen/execute_api/handlers/resource_router.py +1 -2
  64. localstack/services/apigateway/next_gen/execute_api/integrations/aws.py +3 -0
  65. localstack/services/apigateway/next_gen/execute_api/integrations/http.py +3 -3
  66. localstack/services/apigateway/next_gen/execute_api/template_mapping.py +2 -2
  67. localstack/services/apigateway/next_gen/execute_api/test_invoke.py +114 -9
  68. localstack/services/apigateway/next_gen/provider.py +5 -0
  69. localstack/services/apigateway/resource_providers/aws_apigateway_resource.py +1 -1
  70. localstack/services/cloudformation/api_utils.py +4 -8
  71. localstack/services/cloudformation/cfn_utils.py +1 -1
  72. localstack/services/cloudformation/engine/entities.py +14 -4
  73. localstack/services/cloudformation/engine/template_deployer.py +6 -4
  74. localstack/services/cloudformation/engine/transformers.py +6 -4
  75. localstack/services/cloudformation/engine/v2/change_set_model.py +201 -13
  76. localstack/services/cloudformation/engine/v2/change_set_model_describer.py +52 -3
  77. localstack/services/cloudformation/engine/v2/change_set_model_executor.py +117 -76
  78. localstack/services/cloudformation/engine/v2/change_set_model_preproc.py +205 -52
  79. localstack/services/cloudformation/engine/v2/change_set_model_transform.py +350 -116
  80. localstack/services/cloudformation/engine/v2/change_set_model_validator.py +56 -14
  81. localstack/services/cloudformation/engine/v2/change_set_model_visitor.py +1 -0
  82. localstack/services/cloudformation/engine/v2/resolving.py +7 -5
  83. localstack/services/cloudformation/engine/yaml_parser.py +9 -2
  84. localstack/services/cloudformation/provider.py +7 -5
  85. localstack/services/cloudformation/resource_provider.py +7 -1
  86. localstack/services/cloudformation/resources.py +24149 -0
  87. localstack/services/cloudformation/service_models.py +2 -2
  88. localstack/services/cloudformation/v2/entities.py +19 -9
  89. localstack/services/cloudformation/v2/provider.py +336 -106
  90. localstack/services/cloudformation/v2/types.py +13 -7
  91. localstack/services/cloudformation/v2/utils.py +4 -1
  92. localstack/services/cloudwatch/alarm_scheduler.py +4 -1
  93. localstack/services/cloudwatch/provider.py +18 -13
  94. localstack/services/cloudwatch/provider_v2.py +25 -28
  95. localstack/services/dynamodb/packages.py +2 -1
  96. localstack/services/dynamodb/provider.py +42 -0
  97. localstack/services/dynamodb/server.py +2 -2
  98. localstack/services/dynamodb/v2/provider.py +42 -0
  99. localstack/services/ecr/resource_providers/aws_ecr_repository.py +5 -2
  100. localstack/services/edge.py +1 -1
  101. localstack/services/es/provider.py +2 -2
  102. localstack/services/events/event_rule_engine.py +31 -13
  103. localstack/services/events/models.py +4 -5
  104. localstack/services/events/provider.py +17 -14
  105. localstack/services/events/target.py +17 -9
  106. localstack/services/events/v1/provider.py +5 -5
  107. localstack/services/firehose/provider.py +14 -4
  108. localstack/services/iam/provider.py +11 -116
  109. localstack/services/iam/resources/policy_simulator.py +133 -0
  110. localstack/services/kinesis/models.py +15 -2
  111. localstack/services/kinesis/provider.py +86 -3
  112. localstack/services/kms/provider.py +14 -5
  113. localstack/services/lambda_/api_utils.py +6 -3
  114. localstack/services/lambda_/invocation/docker_runtime_executor.py +1 -1
  115. localstack/services/lambda_/invocation/event_manager.py +1 -1
  116. localstack/services/lambda_/invocation/internal_sqs_queue.py +5 -9
  117. localstack/services/lambda_/invocation/lambda_models.py +10 -7
  118. localstack/services/lambda_/invocation/lambda_service.py +5 -1
  119. localstack/services/lambda_/packages.py +1 -1
  120. localstack/services/lambda_/provider.py +4 -3
  121. localstack/services/lambda_/provider_utils.py +1 -1
  122. localstack/services/logs/provider.py +36 -19
  123. localstack/services/moto.py +2 -1
  124. localstack/services/opensearch/cluster.py +15 -7
  125. localstack/services/opensearch/packages.py +26 -7
  126. localstack/services/opensearch/provider.py +8 -2
  127. localstack/services/opensearch/versions.py +56 -7
  128. localstack/services/plugins.py +11 -7
  129. localstack/services/providers.py +10 -2
  130. localstack/services/redshift/provider.py +0 -21
  131. localstack/services/s3/constants.py +5 -2
  132. localstack/services/s3/cors.py +4 -4
  133. localstack/services/s3/models.py +1 -1
  134. localstack/services/s3/notifications.py +55 -39
  135. localstack/services/s3/presigned_url.py +35 -54
  136. localstack/services/s3/provider.py +73 -15
  137. localstack/services/s3/utils.py +42 -22
  138. localstack/services/s3/validation.py +46 -32
  139. localstack/services/s3/website_hosting.py +4 -2
  140. localstack/services/ses/provider.py +18 -8
  141. localstack/services/sns/constants.py +7 -1
  142. localstack/services/sns/executor.py +9 -2
  143. localstack/services/sns/provider.py +8 -5
  144. localstack/services/sns/publisher.py +31 -16
  145. localstack/services/sns/v2/models.py +167 -0
  146. localstack/services/sns/v2/provider.py +867 -0
  147. localstack/services/sns/v2/utils.py +130 -0
  148. localstack/services/sqs/constants.py +1 -1
  149. localstack/services/sqs/developer_api.py +205 -0
  150. localstack/services/sqs/models.py +48 -5
  151. localstack/services/sqs/provider.py +38 -311
  152. localstack/services/sqs/query_api.py +6 -2
  153. localstack/services/sqs/utils.py +121 -2
  154. localstack/services/ssm/provider.py +1 -1
  155. localstack/services/stepfunctions/asl/component/intrinsic/member.py +1 -1
  156. localstack/services/stepfunctions/asl/component/state/state_choice/comparison/comparison.py +5 -11
  157. localstack/services/stepfunctions/asl/component/state/state_choice/state_choice.py +2 -2
  158. localstack/services/stepfunctions/asl/component/state/state_execution/state_map/state_map.py +2 -2
  159. localstack/services/stepfunctions/asl/component/state/state_execution/state_parallel/state_parallel.py +1 -1
  160. localstack/services/stepfunctions/asl/component/state/state_execution/state_task/state_task.py +2 -2
  161. localstack/services/stepfunctions/asl/component/state/state_fail/state_fail.py +1 -1
  162. localstack/services/stepfunctions/asl/component/state/state_pass/state_pass.py +2 -2
  163. localstack/services/stepfunctions/asl/component/state/state_succeed/state_succeed.py +1 -1
  164. localstack/services/stepfunctions/asl/component/state/state_wait/state_wait.py +1 -1
  165. localstack/services/stepfunctions/asl/eval/environment.py +1 -1
  166. localstack/services/stepfunctions/asl/jsonata/jsonata.py +1 -1
  167. localstack/services/stepfunctions/backend/execution.py +2 -1
  168. localstack/services/stores.py +1 -1
  169. localstack/services/transcribe/provider.py +6 -1
  170. localstack/state/codecs.py +61 -0
  171. localstack/state/core.py +11 -5
  172. localstack/state/pickle.py +10 -49
  173. localstack/testing/aws/cloudformation_utils.py +1 -1
  174. localstack/testing/pytest/cloudformation/fixtures.py +3 -3
  175. localstack/testing/pytest/cloudformation/transformers.py +0 -0
  176. localstack/testing/pytest/container.py +4 -5
  177. localstack/testing/pytest/fixtures.py +33 -31
  178. localstack/testing/pytest/in_memory_localstack.py +0 -4
  179. localstack/testing/pytest/marking.py +38 -11
  180. localstack/testing/pytest/stepfunctions/utils.py +4 -3
  181. localstack/testing/pytest/util.py +1 -1
  182. localstack/testing/pytest/validation_tracking.py +1 -2
  183. localstack/testing/snapshots/transformer_utility.py +6 -1
  184. localstack/utils/analytics/events.py +2 -2
  185. localstack/utils/analytics/metadata.py +6 -4
  186. localstack/utils/analytics/metrics/counter.py +8 -15
  187. localstack/utils/analytics/publisher.py +1 -2
  188. localstack/utils/analytics/service_providers.py +19 -0
  189. localstack/utils/analytics/service_request_aggregator.py +2 -2
  190. localstack/utils/archives.py +11 -11
  191. localstack/utils/asyncio.py +2 -2
  192. localstack/utils/aws/arns.py +24 -29
  193. localstack/utils/aws/aws_responses.py +8 -8
  194. localstack/utils/aws/aws_stack.py +2 -3
  195. localstack/utils/aws/dead_letter_queue.py +1 -5
  196. localstack/utils/aws/message_forwarding.py +1 -2
  197. localstack/utils/aws/request_context.py +4 -5
  198. localstack/utils/aws/resources.py +1 -1
  199. localstack/utils/aws/templating.py +1 -1
  200. localstack/utils/batch_policy.py +3 -3
  201. localstack/utils/bootstrap.py +21 -13
  202. localstack/utils/catalog/catalog.py +139 -0
  203. localstack/utils/catalog/catalog_loader.py +119 -0
  204. localstack/utils/catalog/common.py +58 -0
  205. localstack/utils/catalog/plugins.py +28 -0
  206. localstack/utils/cloudwatch/cloudwatch_util.py +5 -5
  207. localstack/utils/collections.py +7 -8
  208. localstack/utils/config_listener.py +1 -1
  209. localstack/utils/container_networking.py +2 -3
  210. localstack/utils/container_utils/container_client.py +135 -136
  211. localstack/utils/container_utils/docker_cmd_client.py +85 -69
  212. localstack/utils/container_utils/docker_sdk_client.py +69 -66
  213. localstack/utils/crypto.py +10 -10
  214. localstack/utils/diagnose.py +3 -4
  215. localstack/utils/docker_utils.py +9 -5
  216. localstack/utils/files.py +33 -13
  217. localstack/utils/functions.py +4 -3
  218. localstack/utils/http.py +11 -11
  219. localstack/utils/json.py +20 -6
  220. localstack/utils/kinesis/kinesis_connector.py +2 -1
  221. localstack/utils/net.py +15 -9
  222. localstack/utils/no_exit_argument_parser.py +2 -2
  223. localstack/utils/numbers.py +9 -2
  224. localstack/utils/objects.py +7 -6
  225. localstack/utils/patch.py +10 -3
  226. localstack/utils/run.py +12 -11
  227. localstack/utils/scheduler.py +11 -11
  228. localstack/utils/server/tcp_proxy.py +2 -2
  229. localstack/utils/serving.py +3 -4
  230. localstack/utils/strings.py +15 -16
  231. localstack/utils/sync.py +126 -1
  232. localstack/utils/tagging.py +8 -6
  233. localstack/utils/testutil.py +8 -8
  234. localstack/utils/threads.py +2 -2
  235. localstack/utils/time.py +12 -4
  236. localstack/utils/urls.py +1 -3
  237. localstack/utils/xray/traceid.py +1 -1
  238. localstack/version.py +16 -3
  239. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/METADATA +18 -14
  240. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/RECORD +248 -239
  241. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/entry_points.txt +8 -4
  242. localstack_core-4.10.1.dev12.dist-info/plux.json +1 -0
  243. localstack/packages/terraform.py +0 -46
  244. localstack/services/cloudformation/deploy.html +0 -144
  245. localstack/services/cloudformation/deploy_ui.py +0 -47
  246. localstack/services/cloudformation/plugins.py +0 -12
  247. localstack_core-4.7.1.dev49.dist-info/plux.json +0 -1
  248. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack +0 -0
  249. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack-supervisor +0 -0
  250. {localstack_core-4.7.1.dev49.data → localstack_core-4.10.1.dev12.data}/scripts/localstack.bat +0 -0
  251. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/WHEEL +0 -0
  252. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/licenses/LICENSE.txt +0 -0
  253. {localstack_core-4.7.1.dev49.dist-info → localstack_core-4.10.1.dev12.dist-info}/top_level.txt +0 -0
@@ -13,20 +13,26 @@ class EngineParameter(TypedDict):
13
13
  given_value: NotRequired[str | None]
14
14
  resolved_value: NotRequired[str | None]
15
15
  default_value: NotRequired[str | None]
16
+ no_echo: NotRequired[bool | None]
16
17
 
17
18
 
18
19
  def engine_parameter_value(parameter: EngineParameter) -> str:
19
- value = parameter.get("given_value") or parameter.get("default_value")
20
- if value is None:
21
- raise RuntimeError("Parameter value is None")
20
+ given_value = parameter.get("given_value")
21
+ if given_value is not None:
22
+ return given_value
22
23
 
23
- return value
24
+ default_value = parameter.get("default_value")
25
+ if default_value is not None:
26
+ return default_value
27
+
28
+ raise RuntimeError("Parameter value is None")
24
29
 
25
30
 
26
31
  class ResolvedResource(TypedDict):
27
32
  LogicalResourceId: str
28
33
  Type: str
29
34
  Properties: dict
30
- ResourceStatus: ResourceStatus
31
- PhysicalResourceId: str | None
32
- LastUpdatedTimestamp: datetime | None
35
+ LastUpdatedTimestamp: datetime
36
+ ResourceStatus: NotRequired[ResourceStatus]
37
+ PhysicalResourceId: NotRequired[str]
38
+ ResourceStatusReason: NotRequired[str]
@@ -2,4 +2,7 @@ from localstack import config
2
2
 
3
3
 
4
4
  def is_v2_engine() -> bool:
5
- return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation") == "engine-v2"
5
+ return config.SERVICE_PROVIDER_CONFIG.get_provider("cloudformation") not in {
6
+ "engine-legacy",
7
+ "engine-legacy_pro",
8
+ }
@@ -69,7 +69,10 @@ class AlarmScheduler:
69
69
  schedule_period = evaluation_periods * period
70
70
 
71
71
  def on_error(e):
72
- LOG.exception("Error executing scheduled alarm", exc_info=e)
72
+ if LOG.isEnabledFor(logging.DEBUG):
73
+ LOG.exception("Error executing scheduled alarm", exc_info=e)
74
+ else:
75
+ LOG.error("Error executing scheduled alarm")
73
76
 
74
77
  task = self.scheduler.schedule(
75
78
  func=calculate_alarm_state,
@@ -1,11 +1,11 @@
1
1
  import json
2
2
  import logging
3
3
  import uuid
4
+ from datetime import datetime
4
5
  from typing import Any
5
- from xml.sax.saxutils import escape
6
6
 
7
7
  from moto.cloudwatch import cloudwatch_backends
8
- from moto.cloudwatch.models import CloudWatchBackend, FakeAlarm, MetricDatum
8
+ from moto.cloudwatch.models import Alarm, CloudWatchBackend, MetricDatum
9
9
 
10
10
  from localstack.aws.accounts import get_account_id_from_access_key_id
11
11
  from localstack.aws.api import CommonServiceException, RequestContext, handler
@@ -54,7 +54,7 @@ MOTO_INITIAL_UNCHECKED_REASON = "Unchecked: Initial alarm creation"
54
54
  LOG = logging.getLogger(__name__)
55
55
 
56
56
 
57
- @patch(target=FakeAlarm.update_state)
57
+ @patch(target=Alarm.update_state)
58
58
  def update_state(target, self, reason, reason_data, state_value):
59
59
  if reason_data is None:
60
60
  reason_data = ""
@@ -127,9 +127,7 @@ def put_metric_alarm(
127
127
  threshold_metric_id: str | None = None,
128
128
  rule: str | None = None,
129
129
  tags: list[dict[str, str]] | None = None,
130
- ) -> FakeAlarm:
131
- if description:
132
- description = escape(description)
130
+ ) -> Alarm:
133
131
  return target(
134
132
  self,
135
133
  name,
@@ -158,7 +156,7 @@ def put_metric_alarm(
158
156
  )
159
157
 
160
158
 
161
- def create_metric_data_query_from_alarm(alarm: FakeAlarm):
159
+ def create_metric_data_query_from_alarm(alarm: Alarm):
162
160
  # TODO may need to be adapted for other use cases
163
161
  # verified return value with a snapshot test
164
162
  return [
@@ -179,7 +177,7 @@ def create_metric_data_query_from_alarm(alarm: FakeAlarm):
179
177
 
180
178
 
181
179
  def create_message_response_update_state_lambda(
182
- alarm: FakeAlarm, old_state, old_state_reason, old_state_timestamp
180
+ alarm: Alarm, old_state, old_state_reason, old_state_timestamp
183
181
  ):
184
182
  response = {
185
183
  "accountId": extract_account_id_from_arn(alarm.alarm_arn),
@@ -189,12 +187,12 @@ def create_message_response_update_state_lambda(
189
187
  "state": {
190
188
  "value": alarm.state_value,
191
189
  "reason": alarm.state_reason,
192
- "timestamp": alarm.state_updated_timestamp,
190
+ "timestamp": _to_iso_8601_datetime_with_nanoseconds(alarm.state_updated_timestamp),
193
191
  },
194
192
  "previousState": {
195
193
  "value": old_state,
196
194
  "reason": old_state_reason,
197
- "timestamp": old_state_timestamp,
195
+ "timestamp": _to_iso_8601_datetime_with_nanoseconds(old_state_timestamp),
198
196
  },
199
197
  "configuration": {
200
198
  "description": alarm.description or "",
@@ -204,7 +202,7 @@ def create_message_response_update_state_lambda(
204
202
  ), # TODO: add test with metric_data_queries
205
203
  },
206
204
  },
207
- "time": alarm.state_updated_timestamp,
205
+ "time": _to_iso_8601_datetime_with_nanoseconds(alarm.state_updated_timestamp),
208
206
  "region": alarm.region_name,
209
207
  "source": "aws.cloudwatch",
210
208
  }
@@ -217,10 +215,12 @@ def create_message_response_update_state_sns(alarm, old_state):
217
215
  "OldStateValue": old_state,
218
216
  "AlarmName": alarm.name,
219
217
  "AlarmDescription": alarm.description or "",
220
- "AlarmConfigurationUpdatedTimestamp": alarm.configuration_updated_timestamp,
218
+ "AlarmConfigurationUpdatedTimestamp": _to_iso_8601_datetime_with_nanoseconds(
219
+ alarm.configuration_updated_timestamp
220
+ ),
221
221
  "NewStateValue": alarm.state_value,
222
222
  "NewStateReason": alarm.state_reason,
223
- "StateChangeTime": alarm.state_updated_timestamp,
223
+ "StateChangeTime": _to_iso_8601_datetime_with_nanoseconds(alarm.state_updated_timestamp),
224
224
  # the long-name for 'region' should be used - as we don't have it, we use the short name
225
225
  # which needs to be slightly changed to make snapshot tests work
226
226
  "Region": alarm.region_name.replace("-", " ").capitalize(),
@@ -268,6 +268,11 @@ class ValidationError(CommonServiceException):
268
268
  super().__init__("ValidationError", message, 400, True)
269
269
 
270
270
 
271
+ def _to_iso_8601_datetime_with_nanoseconds(date: datetime | None) -> str | None:
272
+ if date is not None:
273
+ return date.strftime("%Y-%m-%dT%H:%M:%S.%f000Z")
274
+
275
+
271
276
  def _set_alarm_actions(context, alarm_names, enabled):
272
277
  backend = cloudwatch_backends[context.account_id][context.region]
273
278
  for name in alarm_names:
@@ -15,6 +15,7 @@ from localstack.aws.api.cloudwatch import (
15
15
  AlarmTypes,
16
16
  AmazonResourceName,
17
17
  CloudwatchApi,
18
+ ContributorId,
18
19
  DashboardBody,
19
20
  DashboardName,
20
21
  DashboardNamePrefix,
@@ -107,17 +108,11 @@ _STORE_LOCK = threading.RLock()
107
108
  AWS_MAX_DATAPOINTS_ACCEPTED: int = 1440
108
109
 
109
110
 
110
- class ValidationError(CommonServiceException):
111
- # TODO: check this error against AWS (doesn't exist in the API)
111
+ class ValidationException(CommonServiceException):
112
112
  def __init__(self, message: str):
113
113
  super().__init__("ValidationError", message, 400, True)
114
114
 
115
115
 
116
- class InvalidParameterCombination(CommonServiceException):
117
- def __init__(self, message: str):
118
- super().__init__("InvalidParameterCombination", message, 400, True)
119
-
120
-
121
116
  def _validate_parameters_for_put_metric_data(metric_data: MetricData) -> None:
122
117
  for index, metric_item in enumerate(metric_data):
123
118
  indexplusone = index + 1
@@ -245,7 +240,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
245
240
  results: list[MetricDataResult] = []
246
241
  limit = max_datapoints or 100_800
247
242
  messages: MetricDataResultMessages = []
248
- nxt = None
243
+ nxt: str | None = None
249
244
  label_additions = []
250
245
 
251
246
  for diff in LABEL_DIFFERENTIATORS:
@@ -279,14 +274,14 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
279
274
  timestamp_value_dicts = [
280
275
  {
281
276
  "Timestamp": timestamp,
282
- "Value": value,
277
+ "Value": float(value),
283
278
  }
284
279
  for timestamp, value in zip(timestamps, values, strict=False)
285
280
  ]
286
281
 
287
282
  pagination = PaginatedList(timestamp_value_dicts)
288
283
  timestamp_page, nxt = pagination.get_page(
289
- lambda item: item.get("Timestamp"),
284
+ lambda item: str(item.get("Timestamp")),
290
285
  next_token=next_token,
291
286
  page_size=limit,
292
287
  )
@@ -314,6 +309,11 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
314
309
  state_reason_data: StateReasonData = None,
315
310
  **kwargs,
316
311
  ) -> None:
312
+ if state_value not in ("OK", "ALARM", "INSUFFICIENT_DATA"):
313
+ raise ValidationException(
314
+ f"1 validation error detected: Value '{state_value}' at 'stateValue' failed to satisfy constraint: Member must satisfy enum value set: [INSUFFICIENT_DATA, ALARM, OK]"
315
+ )
316
+
317
317
  try:
318
318
  if state_reason_data:
319
319
  state_reason_data = json.loads(state_reason_data)
@@ -332,10 +332,6 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
332
332
  raise ResourceNotFound()
333
333
 
334
334
  old_state = alarm.alarm["StateValue"]
335
- if state_value not in ("OK", "ALARM", "INSUFFICIENT_DATA"):
336
- raise ValidationError(
337
- f"1 validation error detected: Value '{state_value}' at 'stateValue' failed to satisfy constraint: Member must satisfy enum value set: [INSUFFICIENT_DATA, ALARM, OK]"
338
- )
339
335
 
340
336
  old_state_reason = alarm.alarm["StateReason"]
341
337
  old_state_update_timestamp = alarm.alarm["StateUpdatedTimestamp"]
@@ -415,7 +411,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
415
411
  "ignore",
416
412
  "missing",
417
413
  ]:
418
- raise ValidationError(
414
+ raise ValidationException(
419
415
  f"The value {request['TreatMissingData']} is not supported for TreatMissingData parameter. Supported values are [breaching, notBreaching, ignore, missing]."
420
416
  )
421
417
  # do some sanity checks:
@@ -424,7 +420,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
424
420
  value = request.get("Period")
425
421
  if value not in (10, 30):
426
422
  if value % 60 != 0:
427
- raise ValidationError("Period must be 10, 30 or a multiple of 60")
423
+ raise ValidationException("Period must be 10, 30 or a multiple of 60")
428
424
  if request.get("Statistic"):
429
425
  if request.get("Statistic") not in [
430
426
  "SampleCount",
@@ -433,7 +429,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
433
429
  "Minimum",
434
430
  "Maximum",
435
431
  ]:
436
- raise ValidationError(
432
+ raise ValidationException(
437
433
  f"Value '{request.get('Statistic')}' at 'statistic' failed to satisfy constraint: Member must satisfy enum value set: [Maximum, SampleCount, Sum, Minimum, Average]"
438
434
  )
439
435
 
@@ -447,7 +443,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
447
443
  "evaluate",
448
444
  "ignore",
449
445
  ):
450
- raise ValidationError(
446
+ raise ValidationException(
451
447
  f"Option {evaluate_low_sample_count_percentile} is not supported. "
452
448
  "Supported options for parameter EvaluateLowSampleCountPercentile are evaluate and ignore."
453
449
  )
@@ -690,7 +686,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
690
686
  expected_datapoints = (end_time_unix - start_time_unix) / period
691
687
 
692
688
  if expected_datapoints > AWS_MAX_DATAPOINTS_ACCEPTED:
693
- raise InvalidParameterCombination(
689
+ raise InvalidParameterCombinationException(
694
690
  f"You have requested up to {int(expected_datapoints)} datapoints, which exceeds the limit of {AWS_MAX_DATAPOINTS_ACCEPTED}. "
695
691
  f"You may reduce the datapoints requested by increasing Period, or decreasing the time range."
696
692
  )
@@ -737,7 +733,7 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
737
733
  for i, timestamp in enumerate(timestamps):
738
734
  stat_datapoints.setdefault(selected_unit, {})
739
735
  stat_datapoints[selected_unit].setdefault(timestamp, {})
740
- stat_datapoints[selected_unit][timestamp][stat] = values[i]
736
+ stat_datapoints[selected_unit][timestamp][stat] = float(values[i])
741
737
  stat_datapoints[selected_unit][timestamp]["Unit"] = selected_unit
742
738
 
743
739
  datapoints: list[Datapoint] = []
@@ -822,14 +818,15 @@ class CloudwatchProvider(CloudwatchApi, ServiceLifecycleHook):
822
818
  def describe_alarm_history(
823
819
  self,
824
820
  context: RequestContext,
825
- alarm_name: AlarmName = None,
826
- alarm_types: AlarmTypes = None,
827
- history_item_type: HistoryItemType = None,
828
- start_date: Timestamp = None,
829
- end_date: Timestamp = None,
830
- max_records: MaxRecords = None,
831
- next_token: NextToken = None,
832
- scan_by: ScanBy = None,
821
+ alarm_name: AlarmName | None = None,
822
+ alarm_contributor_id: ContributorId | None = None,
823
+ alarm_types: AlarmTypes | None = None,
824
+ history_item_type: HistoryItemType | None = None,
825
+ start_date: Timestamp | None = None,
826
+ end_date: Timestamp | None = None,
827
+ max_records: MaxRecords | None = None,
828
+ next_token: NextToken | None = None,
829
+ scan_by: ScanBy | None = None,
833
830
  **kwargs,
834
831
  ) -> DescribeAlarmHistoryOutput:
835
832
  store = self.get_store(context.account_id, context.region)
@@ -17,7 +17,8 @@ from localstack.utils.run import run
17
17
  DDB_AGENT_JAR_URL = f"{ARTIFACTS_REPO}/raw/e4e8c8e294b1fcda90c678ff6af5d5ebe1f091eb/dynamodb-local-patch/target/ddb-local-loader-0.2.jar"
18
18
  JAVASSIST_JAR_URL = f"{MAVEN_REPO_URL}/org/javassist/javassist/3.30.2-GA/javassist-3.30.2-GA.jar"
19
19
 
20
- DDBLOCAL_URL = "https://d1ni2b6xgvw0s0.cloudfront.net/v3.x/dynamodb_local_latest.zip"
20
+ # URL points to 2.x here - however the latest 3.x builds are available under this URL
21
+ DDBLOCAL_URL = "https://d1ni2b6xgvw0s0.cloudfront.net/v2.x/dynamodb_local_latest.zip"
21
22
 
22
23
 
23
24
  class DynamoDBLocalPackage(Package):
@@ -47,6 +47,8 @@ from localstack.aws.api.dynamodb import (
47
47
  DeleteRequest,
48
48
  DeleteTableOutput,
49
49
  DescribeContinuousBackupsOutput,
50
+ DescribeContributorInsightsInput,
51
+ DescribeContributorInsightsOutput,
50
52
  DescribeGlobalTableOutput,
51
53
  DescribeKinesisStreamingDestinationOutput,
52
54
  DescribeTableOutput,
@@ -746,6 +748,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
746
748
  if "NumberOfDecreasesToday" not in table_description["ProvisionedThroughput"]:
747
749
  table_description["ProvisionedThroughput"]["NumberOfDecreasesToday"] = 0
748
750
 
751
+ if "WarmThroughput" in table_description:
752
+ table_description["WarmThroughput"]["Status"] = "UPDATING"
753
+
749
754
  tags = table_definitions.pop("Tags", [])
750
755
  if tags:
751
756
  get_store(context.account_id, context.region).TABLE_TAGS[table_arn] = {
@@ -763,6 +768,13 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
763
768
  ) -> DeleteTableOutput:
764
769
  global_table_region = self.get_global_table_region(context, table_name)
765
770
 
771
+ self.ensure_table_exists(
772
+ context.account_id,
773
+ global_table_region,
774
+ table_name,
775
+ error_message=f"Requested resource not found: Table: {table_name} not found",
776
+ )
777
+
766
778
  # Limitation note: On AWS, for a replicated table, if the source table is deleted, the replicated tables continue to exist.
767
779
  # This is not the case for LocalStack, where all replicated tables will also be removed if source is deleted.
768
780
 
@@ -823,6 +835,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
823
835
  table_description["TableClassSummary"] = {
824
836
  "TableClass": table_definitions["TableClass"]
825
837
  }
838
+ if warm_throughput := table_definitions.get("WarmThroughput"):
839
+ table_description["WarmThroughput"] = warm_throughput.copy()
840
+ table_description["WarmThroughput"].setdefault("Status", "ACTIVE")
826
841
 
827
842
  if "GlobalSecondaryIndexes" in table_description:
828
843
  for gsi in table_description["GlobalSecondaryIndexes"]:
@@ -835,6 +850,17 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
835
850
  # Terraform depends on this parity for update operations
836
851
  gsi["ProvisionedThroughput"] = default_values | gsi.get("ProvisionedThroughput", {})
837
852
 
853
+ # Set defaults for warm throughput
854
+ if "WarmThroughput" not in table_description:
855
+ billing_mode = table_definitions.get("BillingMode") if table_definitions else None
856
+ table_description["WarmThroughput"] = {
857
+ "ReadUnitsPerSecond": 12000 if billing_mode == "PAY_PER_REQUEST" else 5,
858
+ "WriteUnitsPerSecond": 4000 if billing_mode == "PAY_PER_REQUEST" else 5,
859
+ }
860
+ table_description["WarmThroughput"]["Status"] = (
861
+ table_description.get("TableStatus") or "ACTIVE"
862
+ )
863
+
838
864
  return DescribeTableOutput(
839
865
  Table=select_from_typed_dict(TableDescription, table_description)
840
866
  )
@@ -955,6 +981,22 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
955
981
 
956
982
  return response
957
983
 
984
+ #
985
+ # Contributor Insights
986
+ #
987
+
988
+ @handler("DescribeContributorInsights", expand=False)
989
+ def describe_contributor_insights(
990
+ self,
991
+ context: RequestContext,
992
+ describe_contributor_insights_input: DescribeContributorInsightsInput,
993
+ ) -> DescribeContributorInsightsOutput:
994
+ return DescribeContributorInsightsOutput(
995
+ TableName=describe_contributor_insights_input["TableName"],
996
+ IndexName=describe_contributor_insights_input.get("IndexName"),
997
+ ContributorInsightsStatus="DISABLED",
998
+ )
999
+
958
1000
  #
959
1001
  # Item ops
960
1002
  #
@@ -162,7 +162,7 @@ class DynamodbServer(Server):
162
162
  cmd = [
163
163
  "java",
164
164
  *self._get_java_vm_options(),
165
- "-Xmx%s" % self.heap_size,
165
+ f"-Xmx{self.heap_size}",
166
166
  f"-javaagent:{dynamodblocal_package.get_installer().get_ddb_agent_jar_path()}",
167
167
  f"-Djava.library.path={self.library_path}",
168
168
  "-jar",
@@ -219,7 +219,7 @@ class DynamodbServer(Server):
219
219
  aws_secret_access_key=DEFAULT_AWS_ACCOUNT_ID,
220
220
  ).dynamodb.list_tables()
221
221
  except Exception:
222
- LOG.exception("DynamoDB health check failed")
222
+ LOG.error("DynamoDB health check failed", exc_info=LOG.isEnabledFor(logging.DEBUG))
223
223
  if expect_shutdown:
224
224
  assert out is None
225
225
  else:
@@ -40,6 +40,8 @@ from localstack.aws.api.dynamodb import (
40
40
  DeleteRequest,
41
41
  DeleteTableOutput,
42
42
  DescribeContinuousBackupsOutput,
43
+ DescribeContributorInsightsInput,
44
+ DescribeContributorInsightsOutput,
43
45
  DescribeGlobalTableOutput,
44
46
  DescribeKinesisStreamingDestinationOutput,
45
47
  DescribeTableOutput,
@@ -558,6 +560,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
558
560
  if "NumberOfDecreasesToday" not in table_description["ProvisionedThroughput"]:
559
561
  table_description["ProvisionedThroughput"]["NumberOfDecreasesToday"] = 0
560
562
 
563
+ if "WarmThroughput" in table_description:
564
+ table_description["WarmThroughput"]["Status"] = "UPDATING"
565
+
561
566
  tags = table_definitions.pop("Tags", [])
562
567
  if tags:
563
568
  get_store(context.account_id, context.region).TABLE_TAGS[table_arn] = {
@@ -575,6 +580,13 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
575
580
  ) -> DeleteTableOutput:
576
581
  global_table_region = self.get_global_table_region(context, table_name)
577
582
 
583
+ self.ensure_table_exists(
584
+ context.account_id,
585
+ global_table_region,
586
+ table_name,
587
+ error_message=f"Requested resource not found: Table: {table_name} not found",
588
+ )
589
+
578
590
  # Limitation note: On AWS, for a replicated table, if the source table is deleted, the replicated tables continue to exist.
579
591
  # This is not the case for LocalStack, where all replicated tables will also be removed if source is deleted.
580
592
 
@@ -634,6 +646,9 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
634
646
  table_description["TableClassSummary"] = {
635
647
  "TableClass": table_definitions["TableClass"]
636
648
  }
649
+ if warm_throughput := table_definitions.get("WarmThroughput"):
650
+ table_description["WarmThroughput"] = warm_throughput.copy()
651
+ table_description["WarmThroughput"].setdefault("Status", "ACTIVE")
637
652
 
638
653
  if "GlobalSecondaryIndexes" in table_description:
639
654
  for gsi in table_description["GlobalSecondaryIndexes"]:
@@ -646,6 +661,17 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
646
661
  # Terraform depends on this parity for update operations
647
662
  gsi["ProvisionedThroughput"] = default_values | gsi.get("ProvisionedThroughput", {})
648
663
 
664
+ # Set defaults for warm throughput
665
+ if "WarmThroughput" not in table_description:
666
+ billing_mode = table_definitions.get("BillingMode") if table_definitions else None
667
+ table_description["WarmThroughput"] = {
668
+ "ReadUnitsPerSecond": 12000 if billing_mode == "PAY_PER_REQUEST" else 5,
669
+ "WriteUnitsPerSecond": 4000 if billing_mode == "PAY_PER_REQUEST" else 5,
670
+ }
671
+ table_description["WarmThroughput"]["Status"] = (
672
+ table_description.get("TableStatus") or "ACTIVE"
673
+ )
674
+
649
675
  return DescribeTableOutput(
650
676
  Table=select_from_typed_dict(TableDescription, table_description)
651
677
  )
@@ -757,6 +783,22 @@ class DynamoDBProvider(DynamodbApi, ServiceLifecycleHook):
757
783
 
758
784
  return response
759
785
 
786
+ #
787
+ # Contributor Insights
788
+ #
789
+
790
+ @handler("DescribeContributorInsights", expand=False)
791
+ def describe_contributor_insights(
792
+ self,
793
+ context: RequestContext,
794
+ describe_contributor_insights_input: DescribeContributorInsightsInput,
795
+ ) -> DescribeContributorInsightsOutput:
796
+ return DescribeContributorInsightsOutput(
797
+ TableName=describe_contributor_insights_input["TableName"],
798
+ IndexName=describe_contributor_insights_input.get("IndexName"),
799
+ ContributorInsightsStatus="DISABLED",
800
+ )
801
+
760
802
  #
761
803
  # Item ops
762
804
  #
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from typing import TypedDict
7
7
 
8
8
  import localstack.services.cloudformation.provider_utils as util
9
- from localstack.constants import AWS_REGION_US_EAST_1, DEFAULT_AWS_ACCOUNT_ID
10
9
  from localstack.services.cloudformation.resource_provider import (
11
10
  OperationStatus,
12
11
  ProgressEvent,
@@ -90,6 +89,10 @@ class ECRRepositoryProvider(ResourceProvider[ECRRepositoryProperties]):
90
89
 
91
90
  """
92
91
  model = request.desired_state
92
+ model["RepositoryName"] = (
93
+ model.get("RepositoryName")
94
+ or util.generate_default_name(request.stack_name, request.logical_resource_id).lower()
95
+ )
93
96
 
94
97
  default_repos_per_stack[request.stack_name] = model["RepositoryName"]
95
98
  LOG.warning(
@@ -98,7 +101,7 @@ class ECRRepositoryProvider(ResourceProvider[ECRRepositoryProperties]):
98
101
  model.update(
99
102
  {
100
103
  "Arn": arns.ecr_repository_arn(
101
- model["RepositoryName"], DEFAULT_AWS_ACCOUNT_ID, AWS_REGION_US_EAST_1
104
+ model["RepositoryName"], request.account_id, request.region_name
102
105
  ),
103
106
  "RepositoryUri": "http://localhost:4566",
104
107
  "ImageTagMutability": "MUTABLE",
@@ -72,7 +72,7 @@ def start_component(
72
72
  default_port=constants.DEFAULT_PORT_EDGE,
73
73
  ),
74
74
  )
75
- raise Exception("Unexpected component name '%s' received during start up" % component)
75
+ raise Exception(f"Unexpected component name '{component}' received during start up")
76
76
 
77
77
 
78
78
  def start_proxy(
@@ -3,7 +3,6 @@ from typing import cast
3
3
 
4
4
  from botocore.exceptions import ClientError
5
5
 
6
- from localstack import constants
7
6
  from localstack.aws.api import RequestContext, handler
8
7
  from localstack.aws.api.es import (
9
8
  ARN,
@@ -68,6 +67,7 @@ from localstack.aws.api.opensearch import (
68
67
  VersionString,
69
68
  )
70
69
  from localstack.aws.connect import connect_to
70
+ from localstack.services.opensearch.packages import ELASTICSEARCH_DEFAULT_VERSION
71
71
 
72
72
 
73
73
  def _version_to_opensearch(
@@ -236,7 +236,7 @@ class EsProvider(EsApi):
236
236
  engine_version = (
237
237
  _version_to_opensearch(elasticsearch_version)
238
238
  if elasticsearch_version
239
- else constants.ELASTICSEARCH_DEFAULT_VERSION
239
+ else ELASTICSEARCH_DEFAULT_VERSION
240
240
  )
241
241
  kwargs = {
242
242
  "DomainName": domain_name,
@@ -59,14 +59,21 @@ class EventRuleEngine:
59
59
  for flat_pattern in flat_pattern_conditions
60
60
  )
61
61
 
62
- def _evaluate_condition(self, value, condition, field_exists: bool):
62
+ def _evaluate_condition(self, value: t.Any, condition: t.Any, field_exists: bool) -> bool:
63
63
  if not isinstance(condition, dict):
64
64
  return field_exists and value == condition
65
+
65
66
  elif (must_exist := condition.get("exists")) is not None:
66
67
  # if must_exists is True then field_exists must be True
67
68
  # if must_exists is False then fields_exists must be False
68
69
  return must_exist == field_exists
70
+
69
71
  elif (anything_but := condition.get("anything-but")) is not None:
72
+ if not field_exists:
73
+ # anything-but can handle None `value`, but it needs to differentiate between user-set `null` and
74
+ # missing value
75
+ return False
76
+
70
77
  if isinstance(anything_but, dict):
71
78
  if (not_condition := anything_but.get("prefix")) is not None:
72
79
  predicate = self._evaluate_prefix
@@ -95,6 +102,7 @@ class EventRuleEngine:
95
102
  elif value is None:
96
103
  # the remaining conditions require the value to not be None
97
104
  return False
105
+
98
106
  elif (prefix := condition.get("prefix")) is not None:
99
107
  if isinstance(prefix, dict):
100
108
  if (prefix_equal_ignore_case := prefix.get("equals-ignore-case")) is not None:
@@ -104,7 +112,7 @@ class EventRuleEngine:
104
112
 
105
113
  elif (suffix := condition.get("suffix")) is not None:
106
114
  if isinstance(suffix, dict):
107
- if suffix_equal_ignore_case := suffix.get("equals-ignore-case"):
115
+ if (suffix_equal_ignore_case := suffix.get("equals-ignore-case")) is not None:
108
116
  return self._evaluate_suffix(suffix_equal_ignore_case.lower(), value.lower())
109
117
  else:
110
118
  return self._evaluate_suffix(suffix, value)
@@ -126,19 +134,19 @@ class EventRuleEngine:
126
134
  return False
127
135
 
128
136
  @staticmethod
129
- def _evaluate_prefix(condition: str | list, value: str) -> bool:
130
- return value.startswith(condition)
137
+ def _evaluate_prefix(condition: str | list, value: t.Any) -> bool:
138
+ return isinstance(value, str) and value.startswith(condition)
131
139
 
132
140
  @staticmethod
133
- def _evaluate_suffix(condition: str | list, value: str) -> bool:
134
- return value.endswith(condition)
141
+ def _evaluate_suffix(condition: str | list, value: t.Any) -> bool:
142
+ return isinstance(value, str) and value.endswith(condition)
135
143
 
136
144
  @staticmethod
137
- def _evaluate_equal_ignore_case(condition: str, value: str) -> bool:
138
- return condition.lower() == value.lower()
145
+ def _evaluate_equal_ignore_case(condition: str, value: t.Any) -> bool:
146
+ return isinstance(value, str) and condition.lower() == value.lower()
139
147
 
140
148
  @staticmethod
141
- def _evaluate_cidr(condition: str, value: str) -> bool:
149
+ def _evaluate_cidr(condition: str, value: t.Any) -> bool:
142
150
  try:
143
151
  ip = ipaddress.ip_address(value)
144
152
  return ip in ipaddress.ip_network(condition)
@@ -146,8 +154,10 @@ class EventRuleEngine:
146
154
  return False
147
155
 
148
156
  @staticmethod
149
- def _evaluate_wildcard(condition: str, value: str) -> bool:
150
- return bool(re.match(re.escape(condition).replace("\\*", ".+") + "$", value))
157
+ def _evaluate_wildcard(condition: str, value: t.Any) -> bool:
158
+ return isinstance(value, str) and bool(
159
+ re.match(re.escape(condition).replace("\\*", ".+") + "$", value)
160
+ )
151
161
 
152
162
  @staticmethod
153
163
  def _evaluate_numeric_condition(conditions: list, value: t.Any) -> bool:
@@ -457,10 +467,18 @@ class EventPatternCompiler:
457
467
  return
458
468
 
459
469
  elif operator == "anything-but":
460
- # anything-but can actually contain any kind of simple rule (str, number, and list)
470
+ # anything-but can actually contain any kind of simple rule (str, number, and list) except Null
471
+ if value is None:
472
+ raise InvalidEventPatternException(
473
+ f"{self.error_prefix}Value of anything-but must be an array or single string/number value."
474
+ )
461
475
  if isinstance(value, list):
462
476
  for v in value:
463
- self._validate_rule(v)
477
+ if v is None:
478
+ raise InvalidEventPatternException(
479
+ f"{self.error_prefix}Inside anything but list, start|null|boolean is not supported."
480
+ )
481
+ self._validate_rule(v, from_="anything-but")
464
482
 
465
483
  return
466
484