splunk-soar-sdk 2.3.7__py3-none-any.whl → 3.0.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 (57) 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/vault_attachment.py +6 -6
  40. soar_sdk/models/view.py +10 -13
  41. soar_sdk/params.py +57 -39
  42. soar_sdk/shims/phantom/action_result.py +4 -4
  43. soar_sdk/shims/phantom/base_connector.py +5 -5
  44. soar_sdk/shims/phantom/ph_ipc.py +3 -3
  45. soar_sdk/shims/phantom/vault.py +35 -34
  46. soar_sdk/types.py +3 -2
  47. soar_sdk/views/template_filters.py +4 -4
  48. soar_sdk/views/template_renderer.py +2 -2
  49. soar_sdk/views/view_parser.py +3 -4
  50. soar_sdk/webhooks/models.py +7 -6
  51. soar_sdk/webhooks/routing.py +4 -3
  52. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.0.0.dist-info}/METADATA +5 -6
  53. splunk_soar_sdk-3.0.0.dist-info/RECORD +104 -0
  54. splunk_soar_sdk-2.3.7.dist-info/RECORD +0 -103
  55. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.0.0.dist-info}/WHEEL +0 -0
  56. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.0.0.dist-info}/entry_points.txt +0 -0
  57. {splunk_soar_sdk-2.3.7.dist-info → splunk_soar_sdk-3.0.0.dist-info}/licenses/LICENSE +0 -0
soar_sdk/abstract.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from abc import abstractmethod
2
- from typing import Any, Optional, Union, Generic, TypeVar
2
+ from typing import Any, Generic, TypeVar
3
3
  from collections.abc import Mapping, Iterable, AsyncIterable
4
4
 
5
5
  from soar_sdk.apis.vault import Vault
@@ -8,9 +8,9 @@ from soar_sdk.apis.container import Container
8
8
  from soar_sdk.action_results import ActionOutput
9
9
  import httpx
10
10
  from pydantic.dataclasses import dataclass
11
- from pydantic import validator
11
+ from pydantic import field_validator
12
12
 
13
- JSONType = Union[dict[str, Any], list[Any], str, int, float, bool, None]
13
+ JSONType = dict[str, Any] | list[Any] | str | int | float | bool | None
14
14
  SummaryType = TypeVar("SummaryType", bound=ActionOutput)
15
15
 
16
16
 
@@ -23,7 +23,8 @@ class SOARClientAuth:
23
23
  password: str = ""
24
24
  user_session_token: str = ""
25
25
 
26
- @validator("base_url")
26
+ @field_validator("base_url")
27
+ @classmethod
27
28
  def validate_phantom_url(cls, value: str) -> str:
28
29
  """Validate and format the base URL for the SOAR API."""
29
30
  return (
@@ -74,13 +75,13 @@ class SOARClient(Generic[SummaryType]):
74
75
  self,
75
76
  endpoint: str,
76
77
  *,
77
- params: Optional[Union[dict[str, Any], httpx.QueryParams]] = None,
78
- headers: Optional[dict[str, str]] = None,
79
- cookies: Optional[dict[str, str]] = None,
80
- timeout: Optional[httpx.Timeout] = None,
81
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
78
+ params: dict[str, Any] | httpx.QueryParams | None = None,
79
+ headers: dict[str, str] | None = None,
80
+ cookies: dict[str, str] | None = None,
81
+ timeout: httpx.Timeout | None = None,
82
+ auth: httpx.Auth | tuple[str, str] | None = None,
82
83
  follow_redirects: bool = False,
83
- extensions: Optional[Mapping[str, Any]] = None,
84
+ extensions: Mapping[str, Any] | None = None,
84
85
  ) -> httpx.Response:
85
86
  """Perform a GET request to the specific endpoint using the SOAR client."""
86
87
  response = self.client.get(
@@ -100,19 +101,17 @@ class SOARClient(Generic[SummaryType]):
100
101
  self,
101
102
  endpoint: str,
102
103
  *,
103
- content: Optional[
104
- Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
105
- ] = None,
106
- data: Optional[Mapping[str, Any]] = None,
107
- files: Optional[dict[str, Any]] = None,
108
- json: Optional[JSONType] = None,
109
- params: Optional[dict[str, Any]] = None,
110
- headers: Optional[dict[str, str]] = None,
111
- cookies: Optional[dict[str, str]] = None,
112
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
113
- timeout: Optional[Union[float, httpx.Timeout]] = None,
104
+ content: str | bytes | Iterable[bytes] | AsyncIterable[bytes] | None = None,
105
+ data: Mapping[str, Any] | None = None,
106
+ files: dict[str, Any] | None = None,
107
+ json: JSONType | None = None,
108
+ params: dict[str, Any] | None = None,
109
+ headers: dict[str, str] | None = None,
110
+ cookies: dict[str, str] | None = None,
111
+ auth: httpx.Auth | tuple[str, str] | None = None,
112
+ timeout: float | httpx.Timeout | None = None,
114
113
  follow_redirects: bool = True,
115
- extensions: Optional[Mapping[str, Any]] = None,
114
+ extensions: Mapping[str, Any] | None = None,
116
115
  ) -> httpx.Response:
117
116
  """Perform a POST request to the specific endpoint using the SOAR client."""
118
117
  headers = headers or {}
@@ -138,19 +137,17 @@ class SOARClient(Generic[SummaryType]):
138
137
  self,
139
138
  endpoint: str,
140
139
  *,
141
- content: Optional[
142
- Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]]
143
- ] = None,
144
- data: Optional[Mapping[str, Any]] = None,
145
- files: Optional[dict[str, Any]] = None,
146
- json: Optional[JSONType] = None,
147
- params: Optional[dict[str, Any]] = None,
148
- headers: Optional[dict[str, str]] = None,
149
- cookies: Optional[dict[str, str]] = None,
150
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
151
- timeout: Optional[Union[float, httpx.Timeout]] = None,
140
+ content: str | bytes | Iterable[bytes] | AsyncIterable[bytes] | None = None,
141
+ data: Mapping[str, Any] | None = None,
142
+ files: dict[str, Any] | None = None,
143
+ json: JSONType | None = None,
144
+ params: dict[str, Any] | None = None,
145
+ headers: dict[str, str] | None = None,
146
+ cookies: dict[str, str] | None = None,
147
+ auth: httpx.Auth | tuple[str, str] | None = None,
148
+ timeout: float | httpx.Timeout | None = None,
152
149
  follow_redirects: bool = True,
153
- extensions: Optional[Mapping[str, Any]] = None,
150
+ extensions: Mapping[str, Any] | None = None,
154
151
  ) -> httpx.Response:
155
152
  """Perform a PUT request to the specific endpoint using the SOAR client."""
156
153
  headers = headers or {}
@@ -176,13 +173,13 @@ class SOARClient(Generic[SummaryType]):
176
173
  self,
177
174
  endpoint: str,
178
175
  *,
179
- params: Optional[Union[dict[str, Any], httpx.QueryParams]] = None,
180
- headers: Optional[dict[str, str]] = None,
181
- cookies: Optional[dict[str, str]] = None,
182
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
183
- timeout: Optional[httpx.Timeout] = None,
176
+ params: dict[str, Any] | httpx.QueryParams | None = None,
177
+ headers: dict[str, str] | None = None,
178
+ cookies: dict[str, str] | None = None,
179
+ auth: httpx.Auth | tuple[str, str] | None = None,
180
+ timeout: httpx.Timeout | None = None,
184
181
  follow_redirects: bool = False,
185
- extensions: Optional[Mapping[str, Any]] = None,
182
+ extensions: Mapping[str, Any] | None = None,
186
183
  ) -> httpx.Response:
187
184
  """Perform a DELETE request to the specific endpoint using the SOAR client."""
188
185
  headers = headers or {}
@@ -229,7 +226,7 @@ class SOARClient(Generic[SummaryType]):
229
226
  pass
230
227
 
231
228
  @abstractmethod
232
- def get_summary(self) -> Optional[SummaryType]:
229
+ def get_summary(self) -> SummaryType | None:
233
230
  """Get the summary for the action run."""
234
231
  pass
235
232
 
@@ -1,12 +1,15 @@
1
- from typing import Optional, Union, get_origin, get_args, Any
1
+ from typing import Union, get_origin, get_args, Any
2
2
  from collections.abc import Iterator
3
- from typing_extensions import NotRequired, TypedDict
4
- from pydantic import BaseModel, Field
3
+ from typing_extensions import TypedDict
4
+ from typing import NotRequired
5
+ from pydantic import BaseModel, Field, ConfigDict
5
6
  import itertools
7
+ import types
6
8
 
7
9
  from soar_sdk.compat import remove_when_soar_newer_than
8
10
  from soar_sdk.shims.phantom.action_result import ActionResult as PhantomActionResult
9
11
  from soar_sdk.meta.datatypes import as_datatype
12
+ from soar_sdk.field_utils import parse_json_schema_extra
10
13
 
11
14
  remove_when_soar_newer_than(
12
15
  "7.0.0", "NotRequired from typing_extensions is in typing in Python 3.11+"
@@ -40,7 +43,7 @@ class ActionResult(PhantomActionResult):
40
43
  self,
41
44
  status: bool,
42
45
  message: str,
43
- param: Optional[dict] = None,
46
+ param: dict | None = None,
44
47
  ) -> None:
45
48
  """Initialize an ActionResult with status, message, and optional parameters.
46
49
 
@@ -82,16 +85,16 @@ class OutputFieldSpecification(TypedDict):
82
85
  data_path: str
83
86
  data_type: str
84
87
  contains: NotRequired[list[str]]
85
- example_values: NotRequired[list[Union[str, float, bool]]]
88
+ example_values: NotRequired[list[str | float | bool]]
86
89
  column_name: NotRequired[str]
87
90
  column_order: NotRequired[int]
88
91
 
89
92
 
90
93
  def OutputField(
91
- cef_types: Optional[list[str]] = None,
92
- example_values: Optional[list[Union[str, float, bool]]] = None,
93
- alias: Optional[str] = None,
94
- column_name: Optional[str] = None,
94
+ cef_types: list[str] | None = None,
95
+ example_values: list[str | float | bool] | None = None,
96
+ alias: str | None = None,
97
+ column_name: str | None = None,
95
98
  ) -> Any: # noqa: ANN401
96
99
  """Define metadata for an action output field.
97
100
 
@@ -121,11 +124,18 @@ def OutputField(
121
124
  ... )
122
125
  ... count: int = OutputField(example_values=[1, 5, 10])
123
126
  """
127
+ json_schema_extra: dict[str, Any] = {}
128
+ if cef_types is not None:
129
+ json_schema_extra["cef_types"] = cef_types
130
+ if example_values is not None:
131
+ json_schema_extra["examples"] = example_values
132
+ if column_name is not None:
133
+ json_schema_extra["column_name"] = column_name
134
+
124
135
  return Field(
125
- examples=example_values,
136
+ default=...,
126
137
  alias=alias,
127
- cef_types=cef_types,
128
- column_name=column_name,
138
+ json_schema_extra=json_schema_extra if json_schema_extra else None,
129
139
  )
130
140
 
131
141
 
@@ -158,11 +168,14 @@ class ActionOutput(BaseModel):
158
168
  Nested ActionOutput classes are supported for complex data structures.
159
169
  """
160
170
 
171
+ # Allow instantiation with both field names and aliases for backward compatibility
172
+ model_config = ConfigDict(populate_by_name=True)
173
+
161
174
  @classmethod
162
175
  def _to_json_schema(
163
176
  cls,
164
177
  parent_datapath: str = "action_result.data.*",
165
- column_order_counter: Optional[itertools.count] = None,
178
+ column_order_counter: itertools.count | None = None,
166
179
  ) -> Iterator[OutputFieldSpecification]:
167
180
  """Convert the ActionOutput class to SOAR-compatible JSON schema.
168
181
 
@@ -191,15 +204,18 @@ class ActionOutput(BaseModel):
191
204
  if column_order_counter is None:
192
205
  column_order_counter = itertools.count()
193
206
 
194
- for _field_name, field in cls.__fields__.items():
207
+ for _field_name, field in cls.model_fields.items():
195
208
  field_name = alias if (alias := field.alias) else _field_name
196
209
 
197
210
  field_type = field.annotation
211
+ if field_type is None:
212
+ continue
213
+
198
214
  datapath = parent_datapath + f".{field_name}"
199
215
 
200
216
  # Handle lists and optional types, even nested ones
201
217
  origin = get_origin(field_type)
202
- while origin in [list, Union, Optional]:
218
+ while origin in [list, Union, types.UnionType]:
203
219
  type_args = [
204
220
  arg
205
221
  for arg in get_args(field_type)
@@ -221,6 +237,11 @@ class ActionOutput(BaseModel):
221
237
  field_type = type_args[0]
222
238
  origin = get_origin(field_type)
223
239
 
240
+ if not isinstance(field_type, type):
241
+ raise TypeError(
242
+ f"Output field {field_name} has invalid type annotation: {field_type}"
243
+ )
244
+
224
245
  if issubclass(field_type, ActionOutput):
225
246
  # If the field is another ActionOutput, recursively call _to_json_schema
226
247
  yield from field_type._to_json_schema(datapath, column_order_counter)
@@ -237,15 +258,17 @@ class ActionOutput(BaseModel):
237
258
  data_path=datapath, data_type=type_name
238
259
  )
239
260
 
240
- if cef_types := field.field_info.extra.get("cef_types"):
261
+ json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
262
+
263
+ if cef_types := json_schema_extra.get("cef_types"):
241
264
  schema_field["contains"] = cef_types
242
- if examples := field.field_info.extra.get("examples"):
265
+ if examples := json_schema_extra.get("examples"):
243
266
  schema_field["example_values"] = examples
244
267
 
245
268
  if field_type is bool:
246
269
  schema_field["example_values"] = [True, False]
247
270
 
248
- column_name = field.field_info.extra.get("column_name")
271
+ column_name = json_schema_extra.get("column_name")
249
272
 
250
273
  if column_name is not None:
251
274
  schema_field["column_name"] = column_name
@@ -1,4 +1,4 @@
1
- from typing import Optional, Any
1
+ from typing import Any
2
2
  import os
3
3
 
4
4
  from soar_sdk.compat import remove_when_soar_newer_than
@@ -31,7 +31,7 @@ class ActionsManager(BaseConnector):
31
31
  self.auth_state: dict = {}
32
32
  self.asset_cache: dict = {}
33
33
 
34
- def get_action(self, identifier: str) -> Optional[Action]:
34
+ def get_action(self, identifier: str) -> Action | None:
35
35
  """Convenience method for getting an Action callable from its identifier.
36
36
 
37
37
  Returns None if there are no actions managed by this object matching the given
@@ -60,14 +60,12 @@ class ActionsManager(BaseConnector):
60
60
  """
61
61
  self._actions[action_identifier] = wrapped_function
62
62
 
63
- def handle(
64
- self, input_data: InputSpecification, handle: Optional[int] = None
65
- ) -> str:
63
+ def handle(self, input_data: InputSpecification, handle: int | None = None) -> str:
66
64
  """Runs handling of the input data on connector."""
67
65
  action_id = input_data.identifier
68
66
  if self.get_action(action_id):
69
67
  self.print_progress_message = True
70
- return self._handle_action(input_data.json(), handle or 0)
68
+ return self._handle_action(input_data.model_dump_json(), handle or 0)
71
69
  else:
72
70
  raise RuntimeError(
73
71
  f"Action {action_id} not recognized"
@@ -86,7 +84,7 @@ class ActionsManager(BaseConnector):
86
84
 
87
85
  if handler := self.get_action(action_id):
88
86
  try:
89
- params = handler.meta.parameters.parse_obj(param)
87
+ params = handler.meta.parameters.model_validate(param)
90
88
  except (ValueError, ValidationError) as e:
91
89
  self.save_progress(
92
90
  f"Validation Error - the params data for action could not be parsed: {e!s}"
soar_sdk/apis/utils.py CHANGED
@@ -1,5 +1,5 @@
1
1
  import httpx
2
- from typing import Optional, Any
2
+ from typing import Any
3
3
  from collections.abc import Generator
4
4
 
5
5
 
@@ -11,9 +11,9 @@ def is_client_authenticated(client: httpx.Client) -> bool:
11
11
  def get_request_iter_pages(
12
12
  client: httpx.Client,
13
13
  endpoint: str,
14
- params: Optional[dict] = None,
14
+ params: dict | None = None,
15
15
  page_size: int = 50,
16
- ) -> Generator[Any, None, None]:
16
+ ) -> Generator[Any]:
17
17
  """Iterate through REST JSON results using the provided paging."""
18
18
  params = params or {}
19
19
 
soar_sdk/apis/vault.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, Optional, Union
1
+ from typing import TYPE_CHECKING
2
2
  from soar_sdk.models.vault_attachment import VaultAttachment
3
3
  from soar_sdk.shims.phantom.vault import PhantomVault, VaultBase
4
4
 
@@ -47,9 +47,9 @@ class Vault:
47
47
  def create_attachment(
48
48
  self,
49
49
  container_id: int,
50
- file_content: Union[str, bytes],
50
+ file_content: str | bytes,
51
51
  file_name: str,
52
- metadata: Optional[dict[str, str]] = None,
52
+ metadata: dict[str, str] | None = None,
53
53
  ) -> str:
54
54
  """Create a vault attachment from file content.
55
55
 
@@ -87,7 +87,7 @@ class Vault:
87
87
  container_id: int,
88
88
  file_location: str,
89
89
  file_name: str,
90
- metadata: Optional[dict[str, str]] = None,
90
+ metadata: dict[str, str] | None = None,
91
91
  ) -> str:
92
92
  """Add an existing file to the vault as an attachment.
93
93
 
@@ -121,9 +121,9 @@ class Vault:
121
121
 
122
122
  def get_attachment(
123
123
  self,
124
- vault_id: Optional[str] = None,
125
- file_name: Optional[str] = None,
126
- container_id: Optional[int] = None,
124
+ vault_id: str | None = None,
125
+ file_name: str | None = None,
126
+ container_id: int | None = None,
127
127
  ) -> list[VaultAttachment]:
128
128
  """Retrieve attachment(s) from the vault.
129
129
 
@@ -161,9 +161,9 @@ class Vault:
161
161
 
162
162
  def delete_attachment(
163
163
  self,
164
- vault_id: Optional[str] = None,
165
- file_name: Optional[str] = None,
166
- container_id: Optional[int] = None,
164
+ vault_id: str | None = None,
165
+ file_name: str | None = None,
166
+ container_id: int | None = None,
167
167
  remove_all: bool = False,
168
168
  ) -> list[str]:
169
169
  """Delete attachment(s) from the vault.
soar_sdk/app.py CHANGED
@@ -3,7 +3,8 @@ import inspect
3
3
  import json
4
4
  from pathlib import Path
5
5
  import sys
6
- from typing import Any, Optional, Union, Callable
6
+ from typing import Any
7
+ from collections.abc import Callable
7
8
  from collections.abc import Iterator
8
9
  from zoneinfo import ZoneInfo
9
10
 
@@ -101,7 +102,7 @@ class App:
101
102
  product_name: str,
102
103
  publisher: str,
103
104
  appid: str,
104
- python_version: Optional[Union[list[PythonVersion], str]] = None,
105
+ python_version: list[PythonVersion] | str | None = None,
105
106
  min_phantom_version: str = MIN_PHANTOM_VERSION,
106
107
  fips_compliant: bool = False,
107
108
  asset_cls: type[BaseAsset] = BaseAsset,
@@ -164,13 +165,13 @@ class App:
164
165
  runner = AppCliRunner(self)
165
166
  runner.run()
166
167
 
167
- def handle(self, raw_input_data: str, handle: Optional[int] = None) -> str:
168
+ def handle(self, raw_input_data: str, handle: int | None = None) -> str:
168
169
  """Runs handling of the input data on connector.
169
170
 
170
171
  NOTE: handle is actually a pointer address to spawn's internal state.
171
172
  In versions of SOAR >6.4.1, handle will not be passed to the app.
172
173
  """
173
- input_data = InputSpecification.parse_obj(json.loads(raw_input_data))
174
+ input_data = InputSpecification.model_validate(json.loads(raw_input_data))
174
175
  self._raw_asset_config = input_data.config.get_asset_config()
175
176
 
176
177
  # Decrypt sensitive fields in the asset configuration
@@ -221,27 +222,27 @@ class App:
221
222
  def asset(self) -> BaseAsset:
222
223
  """Returns the asset instance for the app."""
223
224
  if not hasattr(self, "_asset"):
224
- self._asset = self.asset_cls.parse_obj(self._raw_asset_config)
225
+ self._asset = self.asset_cls.model_validate(self._raw_asset_config)
225
226
  return self._asset
226
227
 
227
228
  def register_action(
228
229
  self,
229
230
  /,
230
- action: Union[str, Callable],
231
+ action: str | Callable,
231
232
  *,
232
- name: Optional[str] = None,
233
- identifier: Optional[str] = None,
234
- description: Optional[str] = None,
233
+ name: str | None = None,
234
+ identifier: str | None = None,
235
+ description: str | None = None,
235
236
  verbose: str = "",
236
237
  action_type: str = "generic", # TODO: consider introducing enum type for that
237
238
  read_only: bool = True,
238
- params_class: Optional[type[Params]] = None,
239
- output_class: Optional[type[ActionOutput]] = None,
240
- render_as: Optional[str] = None,
241
- view_handler: Union[str, Callable, None] = None,
242
- view_template: Optional[str] = None,
239
+ params_class: type[Params] | None = None,
240
+ output_class: type[ActionOutput] | None = None,
241
+ render_as: str | None = None,
242
+ view_handler: str | Callable | None = None,
243
+ view_template: str | None = None,
243
244
  versions: str = "EQ(*)",
244
- summary_type: Optional[type[ActionOutput]] = None,
245
+ summary_type: type[ActionOutput] | None = None,
245
246
  enable_concurrency_lock: bool = False,
246
247
  ) -> Action:
247
248
  """Dynamically register an action function defined in another module.
@@ -415,18 +416,18 @@ class App:
415
416
  def action(
416
417
  self,
417
418
  *,
418
- name: Optional[str] = None,
419
- identifier: Optional[str] = None,
420
- description: Optional[str] = None,
419
+ name: str | None = None,
420
+ identifier: str | None = None,
421
+ description: str | None = None,
421
422
  verbose: str = "",
422
423
  action_type: str = "generic", # TODO: consider introducing enum type for that
423
424
  read_only: bool = True,
424
- params_class: Optional[type[Params]] = None,
425
- output_class: Optional[type[ActionOutput]] = None,
426
- render_as: Optional[str] = None,
427
- view_handler: Optional[Callable] = None,
425
+ params_class: type[Params] | None = None,
426
+ output_class: type[ActionOutput] | None = None,
427
+ render_as: str | None = None,
428
+ view_handler: Callable | None = None,
428
429
  versions: str = "EQ(*)",
429
- summary_type: Optional[type[ActionOutput]] = None,
430
+ summary_type: type[ActionOutput] | None = None,
430
431
  enable_concurrency_lock: bool = False,
431
432
  ) -> ActionDecorator:
432
433
  """Decorator for registering an action function.
@@ -497,7 +498,7 @@ class App:
497
498
  def view_handler(
498
499
  self,
499
500
  *,
500
- template: Optional[str] = None,
501
+ template: str | None = None,
501
502
  ) -> ViewHandlerDecorator:
502
503
  """Decorator for custom view functions with output parsing and template rendering.
503
504
 
@@ -525,7 +526,7 @@ class App:
525
526
  return ViewHandlerDecorator(self, template=template)
526
527
 
527
528
  def make_request(
528
- self, output_class: Optional[type[ActionOutput]] = None
529
+ self, output_class: type[ActionOutput] | None = None
529
530
  ) -> MakeRequestDecorator:
530
531
  """Decorator for registering a ``make request`` action function.
531
532
 
@@ -556,7 +557,7 @@ class App:
556
557
  def _validate_params_class(
557
558
  action_name: str,
558
559
  spec: inspect.FullArgSpec,
559
- params_class: Optional[type[Params]] = None,
560
+ params_class: type[Params] | None = None,
560
561
  ) -> type[Params]:
561
562
  """Validates the class used for params argument of the action.
562
563
 
@@ -572,7 +573,7 @@ class App:
572
573
  "Action function must accept at least the params positional argument"
573
574
  )
574
575
  params_arg = spec.args[0]
575
- annotated_params_type: Optional[type] = spec.annotations.get(params_arg)
576
+ annotated_params_type: type | None = spec.annotations.get(params_arg)
576
577
  if annotated_params_type is None:
577
578
  raise TypeError(
578
579
  f"Action {action_name} has no params type set. "
@@ -594,7 +595,7 @@ class App:
594
595
  """
595
596
  # The reason we wrap values in callables is to avoid evaluating any lazy attributes
596
597
  # (like asset) unless they're actually going to be used in the action function.
597
- magic_args: dict[str, Union[object, Callable[[], object]]] = {
598
+ magic_args: dict[str, object | Callable[[], object]] = {
598
599
  "soar": self.soar_client,
599
600
  "asset": lambda: self.asset,
600
601
  }
@@ -626,18 +627,16 @@ class App:
626
627
 
627
628
  @staticmethod
628
629
  def _adapt_action_result(
629
- result: Union[
630
- ActionOutput,
631
- ActionResult,
632
- list[ActionOutput],
633
- Iterator[ActionOutput],
634
- tuple[bool, str],
635
- bool,
636
- ],
630
+ result: ActionOutput
631
+ | ActionResult
632
+ | list[ActionOutput]
633
+ | Iterator[ActionOutput]
634
+ | tuple[bool, str]
635
+ | bool,
637
636
  actions_manager: ActionsManager,
638
- action_params: Optional[Params] = None,
637
+ action_params: Params | None = None,
639
638
  message: str = "",
640
- summary: Optional[ActionOutput] = None,
639
+ summary: ActionOutput | None = None,
641
640
  ) -> bool:
642
641
  """Handles multiple ways of returning response from action.
643
642
 
@@ -657,13 +656,21 @@ class App:
657
656
  )
658
657
  # Handle empty list/iterator case
659
658
  if not statuses:
660
- result = ActionOutput(status=True, message=message)
659
+ # Create ActionResult directly for empty list
660
+ param_dict = action_params.model_dump() if action_params else None
661
+ result = ActionResult(
662
+ status=True,
663
+ message=message,
664
+ param=param_dict,
665
+ )
666
+ if summary:
667
+ result.set_summary(summary.model_dump(by_alias=True))
661
668
  else:
662
669
  return all(statuses)
663
670
 
664
671
  if isinstance(result, ActionOutput):
665
- output_dict = result.dict(by_alias=True)
666
- param_dict = action_params.dict() if action_params else None
672
+ output_dict = result.model_dump(by_alias=True)
673
+ param_dict = action_params.model_dump() if action_params else None
667
674
 
668
675
  result = ActionResult(
669
676
  status=True,
@@ -672,7 +679,7 @@ class App:
672
679
  )
673
680
  result.add_data(output_dict)
674
681
  if summary:
675
- result.set_summary(summary.dict(by_alias=True))
682
+ result.set_summary(summary.model_dump(by_alias=True))
676
683
 
677
684
  if isinstance(result, ActionResult):
678
685
  actions_manager.add_result(result)
@@ -695,14 +702,14 @@ class App:
695
702
 
696
703
  pytest.mark.skip(inner)
697
704
 
698
- webhook_meta: Optional[WebhookMeta] = None
699
- webhook_router: Optional[Router] = None
705
+ webhook_meta: WebhookMeta | None = None
706
+ webhook_router: Router | None = None
700
707
 
701
708
  def enable_webhooks(
702
709
  self,
703
710
  default_requires_auth: bool = True,
704
- default_allowed_headers: Optional[list[str]] = None,
705
- default_ip_allowlist: Optional[list[str]] = None,
711
+ default_allowed_headers: list[str] | None = None,
712
+ default_ip_allowlist: list[str] | None = None,
706
713
  ) -> "App":
707
714
  """Enable webhook functionality for the app.
708
715
 
@@ -731,7 +738,7 @@ class App:
731
738
  default_ip_allowlist = ["0.0.0.0/0", "::/0"]
732
739
 
733
740
  self.webhook_meta = WebhookMeta(
734
- handler=None, # The handler is set by the ManifestProcessor when generating the final manifest
741
+ handler=None,
735
742
  requires_auth=default_requires_auth,
736
743
  allowed_headers=default_allowed_headers,
737
744
  ip_allowlist=default_ip_allowlist,
@@ -742,7 +749,7 @@ class App:
742
749
  return self
743
750
 
744
751
  def webhook(
745
- self, url_pattern: str, allowed_methods: Optional[list[str]] = None
752
+ self, url_pattern: str, allowed_methods: list[str] | None = None
746
753
  ) -> WebhookDecorator:
747
754
  """Decorator for registering a webhook handler."""
748
755
  return WebhookDecorator(self, url_pattern, allowed_methods)
@@ -752,8 +759,8 @@ class App:
752
759
  method: str,
753
760
  headers: dict[str, str],
754
761
  path_parts: list[str],
755
- query: dict[str, Union[str, list[str], None]],
756
- body: Optional[str],
762
+ query: dict[str, str | list[str] | None],
763
+ body: str | None,
757
764
  asset: dict,
758
765
  soar_rest_client: SoarRestClient,
759
766
  ) -> dict:
@@ -801,4 +808,4 @@ class App:
801
808
  raise TypeError(
802
809
  f"Webhook handler must return a WebhookResponse, got {type(response)}"
803
810
  )
804
- return response.dict()
811
+ return response.model_dump()