splunk-soar-sdk 2.3.7__py3-none-any.whl → 3.1.0__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 (58) hide show
  1. soar_sdk/abstract.py +38 -41
  2. soar_sdk/action_results.py +41 -18
  3. soar_sdk/actions_manager.py +5 -7
  4. soar_sdk/apis/utils.py +3 -3
  5. soar_sdk/apis/vault.py +10 -10
  6. soar_sdk/app.py +58 -51
  7. soar_sdk/app_cli_runner.py +8 -8
  8. soar_sdk/app_client.py +10 -10
  9. soar_sdk/asset.py +45 -33
  10. soar_sdk/async_utils.py +2 -2
  11. soar_sdk/cli/init/cli.py +7 -9
  12. soar_sdk/cli/manifests/deserializers.py +15 -15
  13. soar_sdk/cli/manifests/processors.py +4 -10
  14. soar_sdk/cli/manifests/serializers.py +16 -8
  15. soar_sdk/cli/package/cli.py +6 -6
  16. soar_sdk/cli/package/utils.py +1 -1
  17. soar_sdk/code_renderers/action_renderer.py +35 -18
  18. soar_sdk/code_renderers/app_renderer.py +1 -2
  19. soar_sdk/code_renderers/asset_renderer.py +4 -5
  20. soar_sdk/code_renderers/renderer.py +2 -2
  21. soar_sdk/code_renderers/templates/pyproject.toml.jinja +1 -1
  22. soar_sdk/compat.py +6 -6
  23. soar_sdk/decorators/action.py +14 -15
  24. soar_sdk/decorators/make_request.py +4 -3
  25. soar_sdk/decorators/on_poll.py +5 -4
  26. soar_sdk/decorators/test_connectivity.py +2 -2
  27. soar_sdk/decorators/view_handler.py +11 -17
  28. soar_sdk/decorators/webhook.py +1 -2
  29. soar_sdk/exceptions.py +1 -4
  30. soar_sdk/field_utils.py +8 -0
  31. soar_sdk/input_spec.py +13 -17
  32. soar_sdk/logging.py +3 -3
  33. soar_sdk/meta/actions.py +6 -22
  34. soar_sdk/meta/app.py +10 -7
  35. soar_sdk/meta/dependencies.py +48 -42
  36. soar_sdk/meta/webhooks.py +12 -12
  37. soar_sdk/models/artifact.py +20 -23
  38. soar_sdk/models/container.py +30 -33
  39. soar_sdk/models/finding.py +54 -0
  40. soar_sdk/models/vault_attachment.py +6 -6
  41. soar_sdk/models/view.py +10 -13
  42. soar_sdk/params.py +57 -39
  43. soar_sdk/shims/phantom/action_result.py +4 -4
  44. soar_sdk/shims/phantom/base_connector.py +5 -5
  45. soar_sdk/shims/phantom/ph_ipc.py +3 -3
  46. soar_sdk/shims/phantom/vault.py +35 -34
  47. soar_sdk/types.py +3 -2
  48. soar_sdk/views/template_filters.py +4 -4
  49. soar_sdk/views/template_renderer.py +2 -2
  50. soar_sdk/views/view_parser.py +3 -4
  51. soar_sdk/webhooks/models.py +7 -6
  52. soar_sdk/webhooks/routing.py +4 -3
  53. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/METADATA +5 -6
  54. splunk_soar_sdk-3.1.0.dist-info/RECORD +105 -0
  55. splunk_soar_sdk-2.3.7.dist-info/RECORD +0 -103
  56. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/WHEEL +0 -0
  57. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/entry_points.txt +0 -0
  58. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.1.0.dist-info}/licenses/LICENSE +0 -0
soar_sdk/params.py CHANGED
@@ -1,11 +1,14 @@
1
- from typing import Optional, Union, Any, ClassVar
2
- from typing_extensions import NotRequired, TypedDict
1
+ from typing import Any, ClassVar
2
+ from typing_extensions import TypedDict
3
+ from typing import NotRequired
3
4
 
4
- from pydantic.fields import Field, Undefined
5
+ from pydantic import Field
6
+ from pydantic_core import PydanticUndefined
5
7
  from pydantic.main import BaseModel
6
8
 
7
9
  from soar_sdk.compat import remove_when_soar_newer_than
8
10
  from soar_sdk.meta.datatypes import as_datatype
11
+ from soar_sdk.field_utils import parse_json_schema_extra
9
12
 
10
13
  remove_when_soar_newer_than(
11
14
  "7.0.0", "NotRequired from typing_extensions is in typing in Python 3.11+"
@@ -13,16 +16,16 @@ remove_when_soar_newer_than(
13
16
 
14
17
 
15
18
  def Param(
16
- description: Optional[str] = None,
19
+ description: str | None = None,
17
20
  required: bool = True,
18
21
  primary: bool = False,
19
- default: Optional[Any] = None, # noqa: ANN401
20
- value_list: Optional[list] = None,
21
- cef_types: Optional[list] = None,
22
+ default: Any | None = None, # noqa: ANN401
23
+ value_list: list | None = None,
24
+ cef_types: list | None = None,
22
25
  allow_list: bool = False,
23
26
  sensitive: bool = False,
24
- alias: Optional[str] = None,
25
- column_name: Optional[str] = None,
27
+ alias: str | None = None,
28
+ column_name: str | None = None,
26
29
  ) -> Any: # noqa: ANN401
27
30
  """Representation of a single complex action parameter.
28
31
 
@@ -53,20 +56,30 @@ def Param(
53
56
  :param column_name: Optional name for the parameter when displayed in an output table.
54
57
  :return: returns the FieldInfo object as pydantic.Field
55
58
  """
56
- if value_list is None:
57
- value_list = []
59
+ json_schema_extra: dict[str, Any] = {}
60
+ if required is not None:
61
+ json_schema_extra["required"] = required
62
+ if primary is not None:
63
+ json_schema_extra["primary"] = primary
64
+ if value_list:
65
+ json_schema_extra["value_list"] = value_list
66
+ if cef_types is not None:
67
+ json_schema_extra["cef_types"] = cef_types
68
+ if allow_list is not None:
69
+ json_schema_extra["allow_list"] = allow_list
70
+ if sensitive is not None:
71
+ json_schema_extra["sensitive"] = sensitive
72
+ if column_name is not None:
73
+ json_schema_extra["column_name"] = column_name
74
+
75
+ # Use ... for required fields
76
+ field_default: Any = ... if default is None and required else default
58
77
 
59
78
  return Field(
60
- default=default,
79
+ default=field_default,
61
80
  description=description,
62
- required=required,
63
- primary=primary,
64
- value_list=value_list,
65
- cef_types=cef_types,
66
- allow_list=allow_list,
67
- sensitive=sensitive,
68
81
  alias=alias,
69
- column_name=column_name,
82
+ json_schema_extra=json_schema_extra if json_schema_extra else None,
70
83
  )
71
84
 
72
85
 
@@ -82,7 +95,7 @@ class InputFieldSpecification(TypedDict):
82
95
  primary: bool
83
96
  value_list: NotRequired[list[str]]
84
97
  allow_list: bool
85
- default: NotRequired[Union[str, int, float, bool]]
98
+ default: NotRequired[str | int | float | bool]
86
99
  column_name: NotRequired[str]
87
100
  column_order: NotRequired[int]
88
101
 
@@ -103,9 +116,12 @@ class Params(BaseModel):
103
116
  def _to_json_schema(cls) -> dict[str, InputFieldSpecification]:
104
117
  params: dict[str, InputFieldSpecification] = {}
105
118
 
106
- for field_order, (field_name, field) in enumerate(cls.__fields__.items()):
119
+ for field_order, (field_name, field) in enumerate(cls.model_fields.items()):
107
120
  field_type = field.annotation
108
121
 
122
+ if field_type is None:
123
+ raise TypeError(f"Parameter {field_name} has no type annotation")
124
+
109
125
  try:
110
126
  type_name = as_datatype(field_type)
111
127
  except TypeError as e:
@@ -113,14 +129,16 @@ class Params(BaseModel):
113
129
  f"Failed to serialize action parameter {field_name}: {e}"
114
130
  ) from None
115
131
 
116
- if field.field_info.extra.get("sensitive", False):
132
+ json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
133
+
134
+ if json_schema_extra.get("sensitive", False):
117
135
  if field_type is not str:
118
136
  raise TypeError(
119
137
  f"Sensitive parameter {field_name} must be type str, not {field_type.__name__}"
120
138
  )
121
139
  type_name = "password"
122
140
 
123
- if not (description := field.field_info.description):
141
+ if not (description := field.description):
124
142
  description = cls._default_field_description(field_name)
125
143
 
126
144
  params_field = InputFieldSpecification(
@@ -128,19 +146,19 @@ class Params(BaseModel):
128
146
  name=field_name,
129
147
  description=description,
130
148
  data_type=type_name,
131
- required=field.field_info.extra.get("required", True),
132
- primary=field.field_info.extra.get("primary", False),
133
- allow_list=field.field_info.extra.get("allow_list", False),
149
+ required=bool(json_schema_extra.get("required", True)),
150
+ primary=bool(json_schema_extra.get("primary", False)),
151
+ allow_list=bool(json_schema_extra.get("allow_list", False)),
134
152
  )
135
153
 
136
- if cef_types := field.field_info.extra.get("cef_types"):
154
+ if cef_types := json_schema_extra.get("cef_types"):
137
155
  params_field["contains"] = cef_types
138
- if (default := field.field_info.default) and default != Undefined:
156
+ if (default := field.default) not in (PydanticUndefined, None):
139
157
  params_field["default"] = default
140
- if value_list := field.field_info.extra.get("value_list"):
158
+ if value_list := json_schema_extra.get("value_list"):
141
159
  params_field["value_list"] = value_list
142
160
 
143
- params[field.alias] = params_field
161
+ params[field.alias or field_name] = params_field
144
162
 
145
163
  return params
146
164
 
@@ -189,21 +207,21 @@ class MakeRequestParams(Params):
189
207
  "verify_ssl",
190
208
  }
191
209
 
192
- def __init_subclass__(cls, **kwargs: dict[str, Any]) -> None:
210
+ def __init_subclass__(cls, **kwargs: Any) -> None: # noqa: ANN401
193
211
  """Validate that subclasses only define allowed fields."""
194
212
  super().__init_subclass__(**kwargs)
195
- cls._validate_make_request_fields()
196
213
 
197
- @classmethod
198
- def _validate_make_request_fields(cls) -> None:
199
- """Ensure subclasses only define allowed MakeRequest fields."""
214
+ def model_post_init(self, __context: Any) -> None: # noqa: ANN401
215
+ """Ensure model fields are validated after instance is created."""
216
+ super().model_post_init(__context)
200
217
  # Check if any fields are not in the allowed set
201
- invalid_fields = set(cls.__fields__.keys()) - cls._ALLOWED_FIELDS
202
-
218
+ invalid_fields = (
219
+ set(self.__class__.model_fields.keys()) - self.__class__._ALLOWED_FIELDS
220
+ )
203
221
  if invalid_fields:
204
222
  raise TypeError(
205
- f"MakeRequestParams subclass '{cls.__name__}' can only define these fields: "
206
- f"{sorted(cls._ALLOWED_FIELDS)}. Invalid fields: {sorted(invalid_fields)}"
223
+ f"MakeRequestParams subclass '{self.__class__.__name__}' can only define these fields: "
224
+ f"{sorted(self.__class__._ALLOWED_FIELDS)}. Invalid fields: {sorted(invalid_fields)}"
207
225
  )
208
226
 
209
227
  http_method: str = Param(
@@ -5,12 +5,12 @@ try:
5
5
  except ImportError:
6
6
  _soar_is_available = False
7
7
 
8
- from typing import Any, Optional, Union, TYPE_CHECKING
8
+ from typing import Any, TYPE_CHECKING
9
9
 
10
10
  if TYPE_CHECKING or not _soar_is_available:
11
11
 
12
12
  class ActionResult: # type: ignore[no-redef]
13
- def __init__(self, param: Optional[dict] = None) -> None:
13
+ def __init__(self, param: dict | None = None) -> None:
14
14
  self.status = False
15
15
  self.message = ""
16
16
  self.summary: dict[str, Any] = {}
@@ -24,9 +24,9 @@ if TYPE_CHECKING or not _soar_is_available:
24
24
 
25
25
  def set_status(
26
26
  self,
27
- status_code: Union[bool, int],
27
+ status_code: bool | int,
28
28
  _status_message: str = "",
29
- _exception: Optional[Exception] = None,
29
+ _exception: Exception | None = None,
30
30
  ) -> bool:
31
31
  self.status = bool(status_code)
32
32
  self.message = _status_message
@@ -19,7 +19,7 @@ if TYPE_CHECKING or not _soar_is_available:
19
19
  from soar_sdk.shims.phantom.action_result import ActionResult
20
20
  from soar_sdk.shims.phantom.connector_result import ConnectorResult
21
21
 
22
- from typing import Union, Any, Optional
22
+ from typing import Any
23
23
  from contextlib import suppress
24
24
 
25
25
  class BaseConnector: # type: ignore[no-redef]
@@ -71,14 +71,14 @@ if TYPE_CHECKING or not _soar_is_available:
71
71
  def error_print(
72
72
  self,
73
73
  _tag: str,
74
- _dump_object: Union[str, list, dict, ActionResult, Exception] = "",
74
+ _dump_object: str | list | dict | ActionResult | Exception = "",
75
75
  ) -> None:
76
76
  print(_tag, _dump_object)
77
77
 
78
78
  def debug_print(
79
79
  self,
80
80
  _tag: str,
81
- _dump_object: Union[str, list, dict, ActionResult, Exception] = "",
81
+ _dump_object: str | list | dict | ActionResult | Exception = "",
82
82
  ) -> None:
83
83
  print(_tag, _dump_object)
84
84
 
@@ -109,12 +109,12 @@ if TYPE_CHECKING or not _soar_is_available:
109
109
 
110
110
  def save_container(
111
111
  self, container: dict, fail_on_duplicate: bool = False
112
- ) -> tuple[bool, str, Optional[int]]:
112
+ ) -> tuple[bool, str, int | None]:
113
113
  return True, "Container saved successfully", 1
114
114
 
115
115
  def save_artifacts(
116
116
  self, artifacts: list[dict]
117
- ) -> tuple[bool, str, Union[Optional[int], list[int]]]:
117
+ ) -> tuple[bool, str, int | None | list[int]]:
118
118
  return True, "Artifacts saved successfully", [1]
119
119
 
120
120
  def get_config(self) -> dict:
@@ -5,7 +5,7 @@ try:
5
5
  except ImportError:
6
6
  _soar_is_available = False
7
7
 
8
- from typing import TYPE_CHECKING, Optional
8
+ from typing import TYPE_CHECKING
9
9
 
10
10
  if TYPE_CHECKING or not _soar_is_available:
11
11
  from soar_sdk.shims.phantom.install_info import get_product_version
@@ -34,12 +34,12 @@ if TYPE_CHECKING or not _soar_is_available:
34
34
 
35
35
  @staticmethod
36
36
  def sendstatus(
37
- handle: Optional[int], status: int, message: str, flag: bool
37
+ handle: int | None, status: int, message: str, flag: bool
38
38
  ) -> None:
39
39
  print(message)
40
40
 
41
41
  @staticmethod
42
- def debugprint(handle: Optional[int], message: str, level: int) -> None:
42
+ def debugprint(handle: int | None, message: str, level: int) -> None:
43
43
  print(message)
44
44
 
45
45
  @staticmethod
@@ -1,9 +1,10 @@
1
- from typing import TYPE_CHECKING, Any, Optional, Union
1
+ from typing import TYPE_CHECKING, Any
2
2
 
3
3
  if TYPE_CHECKING:
4
4
  from soar_sdk.abstract import SOARClient
5
5
  from abc import abstractmethod
6
6
  from soar_sdk.exceptions import SoarAPIError
7
+ from datetime import UTC
7
8
 
8
9
 
9
10
  class VaultBase:
@@ -19,9 +20,9 @@ class VaultBase:
19
20
  def create_attachment(
20
21
  self,
21
22
  container_id: int,
22
- file_content: Union[str, bytes],
23
+ file_content: str | bytes,
23
24
  file_name: str,
24
- metadata: Optional[dict[str, str]] = None,
25
+ metadata: dict[str, str] | None = None,
25
26
  ) -> str:
26
27
  """Creates a vault attachment from file content. This differs from add_attachment because it doesn't require the file to exist locally."""
27
28
  pass
@@ -32,7 +33,7 @@ class VaultBase:
32
33
  container_id: int,
33
34
  file_location: str,
34
35
  file_name: str,
35
- metadata: Optional[dict[str, str]] = None,
36
+ metadata: dict[str, str] | None = None,
36
37
  ) -> str:
37
38
  """Add an attachment to vault. This requires the file to exist locally."""
38
39
  pass
@@ -40,9 +41,9 @@ class VaultBase:
40
41
  @abstractmethod
41
42
  def get_attachment(
42
43
  self,
43
- vault_id: Optional[str] = None,
44
- file_name: Optional[str] = None,
45
- container_id: Optional[int] = None,
44
+ vault_id: str | None = None,
45
+ file_name: str | None = None,
46
+ container_id: int | None = None,
46
47
  download_file: bool = True,
47
48
  ) -> list[dict[str, Any]]:
48
49
  """Returns vault attachments based on the provided query parameters."""
@@ -51,9 +52,9 @@ class VaultBase:
51
52
  @abstractmethod
52
53
  def delete_attachment(
53
54
  self,
54
- vault_id: Optional[str] = None,
55
- file_name: Optional[str] = None,
56
- container_id: Optional[int] = None,
55
+ vault_id: str | None = None,
56
+ file_name: str | None = None,
57
+ container_id: int | None = None,
57
58
  remove_all: bool = False,
58
59
  ) -> list[str]:
59
60
  """Deletes vault attachments based on the provided query parameters."""
@@ -79,9 +80,9 @@ if _soar_is_available:
79
80
  def create_attachment(
80
81
  self,
81
82
  container_id: int,
82
- file_content: Union[str, bytes],
83
+ file_content: str | bytes,
83
84
  file_name: str,
84
- metadata: Optional[dict[str, str]] = None,
85
+ metadata: dict[str, str] | None = None,
85
86
  ) -> str:
86
87
  resp_json = Vault.create_attachment(
87
88
  file_content, container_id, file_name, metadata
@@ -96,7 +97,7 @@ if _soar_is_available:
96
97
  container_id: int,
97
98
  file_location: str,
98
99
  file_name: str,
99
- metadata: Optional[dict[str, str]] = None,
100
+ metadata: dict[str, str] | None = None,
100
101
  ) -> str:
101
102
  resp_json = vault_add(container_id, file_location, file_name, metadata)
102
103
  if not resp_json.get("succeeded"):
@@ -106,9 +107,9 @@ if _soar_is_available:
106
107
 
107
108
  def get_attachment(
108
109
  self,
109
- vault_id: Optional[str] = None,
110
- file_name: Optional[str] = None,
111
- container_id: Optional[int] = None,
110
+ vault_id: str | None = None,
111
+ file_name: str | None = None,
112
+ container_id: int | None = None,
112
113
  download_file: bool = True,
113
114
  ) -> list[dict[str, Any]]:
114
115
  success, _, attachment = vault_info(
@@ -120,9 +121,9 @@ if _soar_is_available:
120
121
 
121
122
  def delete_attachment(
122
123
  self,
123
- vault_id: Optional[str] = None,
124
- file_name: Optional[str] = None,
125
- container_id: Optional[int] = None,
124
+ vault_id: str | None = None,
125
+ file_name: str | None = None,
126
+ container_id: int | None = None,
126
127
  remove_all: bool = False,
127
128
  ) -> list[str]:
128
129
  success, message, deleted_file_names = vault_delete(
@@ -143,7 +144,7 @@ else:
143
144
  from pathlib import Path
144
145
  import secrets
145
146
  import random
146
- from datetime import datetime, timezone
147
+ from datetime import datetime
147
148
  import hashlib
148
149
  from soar_sdk.logging import getLogger
149
150
  from soar_sdk.models.vault_attachment import VaultAttachment
@@ -162,9 +163,9 @@ else:
162
163
  def create_attachment(
163
164
  self,
164
165
  container_id: int,
165
- file_content: Union[str, bytes],
166
+ file_content: str | bytes,
166
167
  file_name: str,
167
- metadata: Optional[dict[str, str]] = None,
168
+ metadata: dict[str, str] | None = None,
168
169
  ) -> str:
169
170
  if is_client_authenticated(self.soar_client.client):
170
171
  data = {
@@ -203,7 +204,7 @@ else:
203
204
  container_id=container_id,
204
205
  container="test_container",
205
206
  created_via="upload",
206
- create_time=datetime.now(timezone.utc).isoformat(),
207
+ create_time=datetime.now(UTC).isoformat(),
207
208
  user="Phantom User",
208
209
  vault_document=doc_id,
209
210
  vault_id=vault_id,
@@ -224,7 +225,7 @@ else:
224
225
  container_id: int,
225
226
  file_location: str,
226
227
  file_name: str,
227
- metadata: Optional[dict[str, str]] = None,
228
+ metadata: dict[str, str] | None = None,
228
229
  ) -> str:
229
230
  metadata = metadata or {}
230
231
 
@@ -265,7 +266,7 @@ else:
265
266
  container_id=container_id,
266
267
  container="test_container",
267
268
  created_via="upload",
268
- create_time=datetime.now(timezone.utc).isoformat(),
269
+ create_time=datetime.now(UTC).isoformat(),
269
270
  user="Phantom User",
270
271
  vault_document=doc_id,
271
272
  vault_id=vault_id,
@@ -283,9 +284,9 @@ else:
283
284
 
284
285
  def get_attachment(
285
286
  self,
286
- vault_id: Optional[str] = None,
287
- file_name: Optional[str] = None,
288
- container_id: Optional[int] = None,
287
+ vault_id: str | None = None,
288
+ file_name: str | None = None,
289
+ container_id: int | None = None,
289
290
  download_file: bool = True,
290
291
  ) -> list[dict[str, Any]]:
291
292
  if not any([vault_id, file_name, container_id]):
@@ -295,7 +296,7 @@ else:
295
296
 
296
297
  results = []
297
298
  if is_client_authenticated(self.soar_client.client):
298
- query_params: dict[str, Union[str, int]] = {"pretty": ""}
299
+ query_params: dict[str, str | int] = {"pretty": ""}
299
300
  if vault_id:
300
301
  query_params["_filter_vault_document__hash"] = (
301
302
  f'"{vault_id.lower()}"'
@@ -319,22 +320,22 @@ else:
319
320
  if vault_id:
320
321
  res = self.__storage.get(vault_id)
321
322
  if res:
322
- results.append(res.dict())
323
+ results.append(res.model_dump())
323
324
 
324
325
  if any((container_id, file_name)):
325
326
  for _, res in self.__storage.items():
326
327
  if (
327
328
  file_name and file_name in res.file_path
328
329
  ) or container_id == res.container_id:
329
- results.append(res.dict())
330
+ results.append(res.model_dump())
330
331
 
331
332
  return results
332
333
 
333
334
  def delete_attachment(
334
335
  self,
335
- vault_id: Optional[str] = None,
336
- file_name: Optional[str] = None,
337
- container_id: Optional[int] = None,
336
+ vault_id: str | None = None,
337
+ file_name: str | None = None,
338
+ container_id: int | None = None,
338
339
  remove_all: bool = False,
339
340
  ) -> list[str]:
340
341
  vault_enteries = self.get_attachment(vault_id, file_name, container_id)
soar_sdk/types.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import typing
2
- from typing import Any, Callable, Optional, Protocol
2
+ from typing import Any, Protocol
3
+ from collections.abc import Callable
3
4
 
4
5
  from soar_sdk.meta.actions import ActionMeta
5
6
  from soar_sdk.params import Params
@@ -9,7 +10,7 @@ class Action(Protocol):
9
10
  """Type interface for an action definition."""
10
11
 
11
12
  meta: ActionMeta
12
- params_class: Optional[type[Params]] = None
13
+ params_class: type[Params] | None = None
13
14
 
14
15
  def __call__(self, *args: Any, **kwargs: Any) -> bool: # noqa: ANN401
15
16
  """Execute the action function."""
@@ -10,7 +10,7 @@ import uuid
10
10
  import humanize
11
11
  import bleach # type: ignore[import-untyped]
12
12
  from datetime import datetime, timedelta
13
- from typing import Optional, TypeVar, Union
13
+ from typing import TypeVar
14
14
  from collections.abc import Iterable
15
15
  from collections.abc import Iterator
16
16
  from jinja2 import Environment
@@ -77,7 +77,7 @@ def by_key(dictionary: dict, key: str) -> str:
77
77
  return dictionary.get(key, "")
78
78
 
79
79
 
80
- def by_nested_key(dictionary: dict, key: str) -> Optional[str]:
80
+ def by_nested_key(dictionary: dict, key: str) -> str | None:
81
81
  """Get nested dictionary value by space-separated key."""
82
82
  split_key = key.split()
83
83
  src = dictionary.get(split_key[0], None)
@@ -97,7 +97,7 @@ def typeof(item: object) -> type:
97
97
  return type(item)
98
98
 
99
99
 
100
- def safe_intcomma(value: Union[str, int]) -> str:
100
+ def safe_intcomma(value: str | int) -> str:
101
101
  """Format integer with commas, safely handling non-integers."""
102
102
  try:
103
103
  return f"{int(value):,}"
@@ -143,7 +143,7 @@ def to_json(obj: object) -> str:
143
143
  return json.dumps(obj)
144
144
 
145
145
 
146
- def absval(obj: Union[int, float]) -> Union[int, float]:
146
+ def absval(obj: int | float) -> int | float:
147
147
  """Get absolute value."""
148
148
  return abs(obj)
149
149
 
@@ -1,6 +1,6 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from pathlib import Path
3
- from typing import Any, Optional
3
+ from typing import Any
4
4
  from jinja2 import Environment, FileSystemLoader, select_autoescape
5
5
  from soar_sdk.views.template_filters import setup_jinja_env
6
6
  from soar_sdk.paths import SDK_TEMPLATES
@@ -107,7 +107,7 @@ class JinjaTemplateRenderer(TemplateRenderer):
107
107
 
108
108
 
109
109
  def get_template_renderer(
110
- engine: Optional[str] = None, templates_dir: Optional[str] = None
110
+ engine: str | None = None, templates_dir: str | None = None
111
111
  ) -> TemplateRenderer:
112
112
  """Factory function to get the appropriate template renderer.
113
113
 
@@ -1,13 +1,12 @@
1
1
  from typing import (
2
2
  Any,
3
- Callable,
4
3
  TypeVar,
5
- Union,
6
4
  get_origin,
7
5
  get_args,
8
6
  Generic,
9
7
  cast,
10
8
  )
9
+ from collections.abc import Callable
11
10
  import inspect
12
11
  from pydantic import BaseModel
13
12
  from soar_sdk.action_results import ActionOutput
@@ -68,7 +67,7 @@ class ViewFunctionParser(Generic[T]):
68
67
  for result in action_results:
69
68
  for data_item in result.get_data():
70
69
  try:
71
- parsed_output = self.output_class.parse_obj(data_item)
70
+ parsed_output = self.output_class.model_validate(data_item)
72
71
  parsed_outputs.append(parsed_output)
73
72
  except Exception as e:
74
73
  output_class_name = self.output_class.__name__
@@ -85,7 +84,7 @@ class ViewFunctionParser(Generic[T]):
85
84
  context: ViewContext,
86
85
  *args: Any, # noqa: ANN401
87
86
  **kwargs: Any, # noqa: ANN401
88
- ) -> Union[str, dict, BaseModel]:
87
+ ) -> str | dict | BaseModel:
89
88
  """Wrapper around the object's view function which massages platform inputs as necessary.
90
89
 
91
90
  Takes the JSON list of AppRun results provided by Splunk SOAR, parses that into
@@ -2,7 +2,8 @@ import json
2
2
  import base64
3
3
  import mimetypes
4
4
 
5
- from typing import Optional, TypeVar, Generic, Any, IO, Callable
5
+ from typing import TypeVar, Generic, Any, IO
6
+ from collections.abc import Callable
6
7
  from pydantic import BaseModel, Field
7
8
 
8
9
  from soar_sdk.asset import BaseAsset
@@ -18,7 +19,7 @@ class WebhookRequest(BaseModel, Generic[AssetType]):
18
19
  headers: dict[str, str]
19
20
  path_parts: list[str]
20
21
  query: dict[str, list[str]]
21
- body: Optional[str]
22
+ body: str | None
22
23
  asset: AssetType
23
24
  soar_base_url: str
24
25
  soar_auth_token: str
@@ -65,7 +66,7 @@ class WebhookResponse(BaseModel):
65
66
  def text_response(
66
67
  content: str,
67
68
  status_code: int = 200,
68
- extra_headers: Optional[dict[str, Any]] = None,
69
+ extra_headers: dict[str, Any] | None = None,
69
70
  ) -> "WebhookResponse":
70
71
  """Build a WebhookResponse object given raw textual content.
71
72
 
@@ -83,7 +84,7 @@ class WebhookResponse(BaseModel):
83
84
  def json_response(
84
85
  content: dict,
85
86
  status_code: int = 200,
86
- extra_headers: Optional[dict[str, Any]] = None,
87
+ extra_headers: dict[str, Any] | None = None,
87
88
  ) -> "WebhookResponse":
88
89
  """Build a WebhookResponse object given a dictionary, to be interpreted as JSON.
89
90
 
@@ -101,9 +102,9 @@ class WebhookResponse(BaseModel):
101
102
  def file_response(
102
103
  fd: IO,
103
104
  filename: str,
104
- content_type: Optional[str] = None,
105
+ content_type: str | None = None,
105
106
  status_code: int = 200,
106
- extra_headers: Optional[dict[str, Any]] = None,
107
+ extra_headers: dict[str, Any] | None = None,
107
108
  ) -> "WebhookResponse":
108
109
  """Build a webhook response using the data in a given open file-like object.
109
110
 
@@ -5,7 +5,8 @@ This module provides a router for mapping URL patterns to handler functions.
5
5
 
6
6
  import re
7
7
  from dataclasses import dataclass
8
- from typing import Callable, TypeVar, Generic, Optional
8
+ from typing import TypeVar, Generic
9
+ from collections.abc import Callable
9
10
  from collections.abc import Sequence
10
11
 
11
12
  from soar_sdk.webhooks.models import WebhookRequest, WebhookResponse
@@ -45,7 +46,7 @@ class Router(Generic[AssetType]):
45
46
  self,
46
47
  pattern: str,
47
48
  handler: Callable[[WebhookRequest], WebhookResponse],
48
- methods: Optional[Sequence[str]] = None,
49
+ methods: Sequence[str] | None = None,
49
50
  ) -> None:
50
51
  """Register a new route with the router.
51
52
 
@@ -146,7 +147,7 @@ class Router(Generic[AssetType]):
146
147
  return False
147
148
 
148
149
  # Check each segment
149
- for part1, part2 in zip(parts1, parts2):
150
+ for part1, part2 in zip(parts1, parts2, strict=False):
150
151
  is_param1 = part1.startswith("<") and part1.endswith(">")
151
152
  is_param2 = part2.startswith("<") and part2.endswith(">")
152
153