splunk-soar-sdk 2.3.6__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 (58) hide show
  1. soar_sdk/abstract.py +38 -41
  2. soar_sdk/action_results.py +41 -18
  3. soar_sdk/actions_manager.py +10 -13
  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 +13 -5
  44. soar_sdk/shims/phantom/install_info.py +15 -2
  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.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/METADATA +5 -6
  54. splunk_soar_sdk-3.0.0.dist-info/RECORD +104 -0
  55. splunk_soar_sdk-2.3.6.dist-info/RECORD +0 -103
  56. {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/WHEEL +0 -0
  57. {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/entry_points.txt +0 -0
  58. {splunk_soar_sdk-2.3.6.dist-info → splunk_soar_sdk-3.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,7 @@
1
1
  import dataclasses
2
2
  import json
3
3
  from pathlib import Path
4
- from typing import Any, NamedTuple, Optional, TypeVar, Union, cast
4
+ from typing import Any, NamedTuple, TypeVar, cast
5
5
  import pydantic
6
6
 
7
7
  from soar_sdk.action_results import ActionOutput, OutputFieldSpecification, OutputField
@@ -94,10 +94,10 @@ class OutputFieldModel:
94
94
 
95
95
  data_path: str
96
96
  data_type: str
97
- contains: Optional[list[str]] = None
98
- example_values: Optional[list[Union[str, float, bool]]] = None
99
- column_name: Optional[str] = None
100
- column_order: Optional[int] = None
97
+ contains: list[str] | None = None
98
+ example_values: list[str | float | bool] | None = None
99
+ column_name: str | None = None
100
+ column_order: int | None = None
101
101
 
102
102
 
103
103
  class DeserializedActionMeta(NamedTuple):
@@ -129,7 +129,7 @@ class ActionDeserializer:
129
129
  )
130
130
  action["output"] = cls.parse_output(action["action"], action.get("output", []))
131
131
  return DeserializedActionMeta(
132
- action_meta=ActionMeta.parse_obj(action),
132
+ action_meta=ActionMeta.model_validate(action),
133
133
  has_custom_view=action.get("render", {}).get("type") == "custom",
134
134
  )
135
135
 
@@ -212,7 +212,7 @@ class ActionDeserializer:
212
212
  @staticmethod
213
213
  def _build_output_structure(
214
214
  datapath_specs: dict[str, OutputFieldModel],
215
- ) -> dict[str, Union[list, dict, OutputFieldModel]]:
215
+ ) -> dict[str, list | dict | OutputFieldModel]:
216
216
  """Parse a datapath string into a dictionary.
217
217
 
218
218
  Args:
@@ -223,10 +223,10 @@ class ActionDeserializer:
223
223
  """
224
224
 
225
225
  def set_nested_value(
226
- field_struct: dict[str, Union[list, dict, OutputFieldModel]],
226
+ field_struct: dict[str, list | dict | OutputFieldModel],
227
227
  path_parts: list[str],
228
228
  field_spec: OutputFieldModel,
229
- ) -> Union[list, dict, OutputFieldModel]:
229
+ ) -> list | dict | OutputFieldModel:
230
230
  """Recursively set a field spec in the nested output field structure."""
231
231
  # Base case: we're at a leaf node and can return the field spec directly
232
232
  if not path_parts:
@@ -240,7 +240,7 @@ class ActionDeserializer:
240
240
 
241
241
  # Recursive case: this portion of the datapath is an object key
242
242
  next_field_struct = cast(
243
- dict[str, Union[list, dict, OutputFieldModel]],
243
+ dict[str, list | dict | OutputFieldModel],
244
244
  field_struct.get(current_key, {}),
245
245
  )
246
246
  field_struct[current_key] = set_nested_value(
@@ -248,7 +248,7 @@ class ActionDeserializer:
248
248
  )
249
249
  return field_struct
250
250
 
251
- MergeT = TypeVar("MergeT", bound=Union[list, dict, OutputFieldModel])
251
+ MergeT = TypeVar("MergeT", bound=list | dict | OutputFieldModel)
252
252
 
253
253
  def merge(base: MergeT, new_structure: MergeT) -> MergeT:
254
254
  """Merge two nested structures, handling arrays and objects."""
@@ -280,12 +280,12 @@ class ActionDeserializer:
280
280
  # Should never happen in reality, hence the pragma, but we handle it gracefully
281
281
  return new_structure # pragma: no cover
282
282
 
283
- result: dict[str, Union[list, dict, OutputFieldModel]] = {}
283
+ result: dict[str, list | dict | OutputFieldModel] = {}
284
284
 
285
285
  for datapath, field_spec in datapath_specs.items():
286
286
  path_parts = datapath.split(".")
287
287
  nested_structure = cast(
288
- dict[str, Union[list, dict, OutputFieldModel]],
288
+ dict[str, list | dict | OutputFieldModel],
289
289
  set_nested_value({}, path_parts, field_spec),
290
290
  )
291
291
  merged = merge(result, nested_structure)
@@ -298,7 +298,7 @@ class ActionDeserializer:
298
298
  def _build_output_class(
299
299
  cls,
300
300
  action_name: str,
301
- output_structure: dict[str, Union[dict, list, OutputFieldModel]],
301
+ output_structure: dict[str, dict | list | OutputFieldModel],
302
302
  ) -> type[ActionOutput]:
303
303
  """Build dynamic pydantic models for an action output, from the output data paths.
304
304
 
@@ -323,7 +323,7 @@ class ActionDeserializer:
323
323
 
324
324
  @classmethod
325
325
  def _build_output_field(
326
- cls, field_name: str, output_structure: Union[list, dict, OutputFieldModel]
326
+ cls, field_name: str, output_structure: list | dict | OutputFieldModel
327
327
  ) -> tuple[str, FieldSpec]:
328
328
  """Build dynamic specs for an action output field, from an output data path.
329
329
 
@@ -1,13 +1,13 @@
1
1
  import importlib
2
2
  import json
3
3
  import toml
4
- from datetime import datetime, timezone
4
+ from datetime import datetime, UTC
5
5
  from pathlib import Path
6
6
  from pprint import pprint
7
7
 
8
8
  from soar_sdk.app import App
9
9
  from soar_sdk.cli.path_utils import context_directory
10
- from soar_sdk.compat import remove_when_soar_newer_than, UPDATE_TIME_FORMAT
10
+ from soar_sdk.compat import UPDATE_TIME_FORMAT
11
11
  from soar_sdk.meta.adapters import TOMLDataAdapter
12
12
  from soar_sdk.meta.app import AppMeta
13
13
  from soar_sdk.meta.dependencies import UvLock
@@ -30,9 +30,7 @@ class ManifestProcessor:
30
30
  app = self.import_app_instance(app_meta)
31
31
  app_meta.configuration = app.asset_cls.to_json_schema()
32
32
  app_meta.actions = app.actions_manager.get_actions_meta_list()
33
- app_meta.utctime_updated = datetime.now(timezone.utc).strftime(
34
- UPDATE_TIME_FORMAT
35
- )
33
+ app_meta.utctime_updated = datetime.now(UTC).strftime(UPDATE_TIME_FORMAT)
36
34
  for field, value in app.app_meta_info.items():
37
35
  setattr(app_meta, field, value)
38
36
 
@@ -43,16 +41,12 @@ class ManifestProcessor:
43
41
  dep for dep in dependencies if dep.name != "splunk-soar-sdk"
44
42
  ]
45
43
 
46
- app_meta.pip39_dependencies, app_meta.pip313_dependencies = (
44
+ app_meta.pip313_dependencies, app_meta.pip314_dependencies = (
47
45
  uv_lock.resolve_dependencies(dependencies)
48
46
  )
49
47
 
50
48
  if app.webhook_meta is not None:
51
- remove_when_soar_newer_than("6.4.0")
52
49
  app_meta.webhook = app.webhook_meta
53
- app_meta.webhook.handler = (
54
- f"{app_meta.main_module.replace(':', '.')}.handle_webhook"
55
- )
56
50
 
57
51
  return app_meta
58
52
 
@@ -1,4 +1,4 @@
1
- from typing import Any, Optional
1
+ from typing import Any
2
2
  from collections.abc import Iterator
3
3
  from logging import getLogger
4
4
  import itertools
@@ -6,6 +6,7 @@ import itertools
6
6
  from soar_sdk.meta.datatypes import as_datatype
7
7
  from soar_sdk.params import Params
8
8
  from soar_sdk.action_results import ActionOutput, OutputFieldSpecification
9
+ from soar_sdk.field_utils import parse_json_schema_extra
9
10
 
10
11
  logger = getLogger(__name__)
11
12
 
@@ -16,7 +17,7 @@ class ParamsSerializer:
16
17
  @staticmethod
17
18
  def get_sorted_fields_keys(params_class: type[Params]) -> list[str]:
18
19
  """Lists the fields of a Params class in order of declaration."""
19
- return list(params_class.__fields__.keys())
20
+ return list(params_class.model_fields.keys())
20
21
 
21
22
  @classmethod
22
23
  def serialize_fields_info(cls, params_class: type[Params]) -> dict[str, Any]:
@@ -30,21 +31,28 @@ class OutputsSerializer:
30
31
  @staticmethod
31
32
  def serialize_parameter_datapaths(
32
33
  params_class: type[Params],
33
- column_order_counter: Optional[itertools.count] = None,
34
+ column_order_counter: itertools.count | None = None,
34
35
  ) -> Iterator[OutputFieldSpecification]:
35
36
  """Serializes the parameter data paths of a Params class to JSON schema."""
36
37
  if column_order_counter is None:
37
38
  column_order_counter = itertools.count()
38
39
 
39
- for field_name, field in params_class.__fields__.items():
40
+ for field_name, field in params_class.model_fields.items():
41
+ annotation = field.annotation
42
+ if annotation is None:
43
+ continue
44
+
40
45
  spec = OutputFieldSpecification(
41
46
  data_path=f"action_result.parameter.{field_name}",
42
- data_type=as_datatype(field.annotation),
47
+ data_type=as_datatype(annotation),
43
48
  )
44
- if cef_types := field.field_info.extra.get("cef_types"):
49
+
50
+ json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
51
+
52
+ if cef_types := json_schema_extra.get("cef_types"):
45
53
  spec["contains"] = cef_types
46
54
 
47
- column_name = field.field_info.extra.get("column_name")
55
+ column_name = json_schema_extra.get("column_name")
48
56
 
49
57
  if column_name is not None:
50
58
  spec["column_name"] = column_name
@@ -56,7 +64,7 @@ class OutputsSerializer:
56
64
  cls,
57
65
  params_class: type[Params],
58
66
  outputs_class: type[ActionOutput],
59
- summary_class: Optional[type[ActionOutput]] = None,
67
+ summary_class: type[ActionOutput] | None = None,
60
68
  ) -> list[OutputFieldSpecification]:
61
69
  """Serializes the data paths of an action to JSON schema."""
62
70
  status = OutputFieldSpecification(
@@ -9,7 +9,7 @@ import json
9
9
  from pathlib import Path
10
10
  import asyncio
11
11
  import time
12
- from typing import Optional, Annotated
12
+ from typing import Annotated
13
13
  from tqdm import tqdm
14
14
  from rich.console import Console
15
15
  from rich.panel import Panel
@@ -67,11 +67,11 @@ def build(
67
67
  ),
68
68
  ],
69
69
  output_file: Annotated[
70
- Optional[Path],
70
+ Path | None,
71
71
  typer.Option("--output-file", "-o", show_default="derived from pyproject.toml"),
72
72
  ] = None,
73
73
  with_sdk_wheel_from: Annotated[
74
- Optional[Path],
74
+ Path | None,
75
75
  typer.Option(
76
76
  "--with-sdk-wheel-from",
77
77
  "-w",
@@ -115,7 +115,7 @@ def build(
115
115
 
116
116
  console.print(f"Generated manifest for app:[green] {app_name}[/]")
117
117
 
118
- def filter_source_files(t: tarfile.TarInfo) -> Optional[tarfile.TarInfo]:
118
+ def filter_source_files(t: tarfile.TarInfo) -> tarfile.TarInfo | None:
119
119
  if t.isdir() and "__pycache__" not in t.name:
120
120
  return t
121
121
  if t.isfile() and t.name.endswith(".py"):
@@ -125,7 +125,7 @@ def build(
125
125
  with tarfile.open(output_file, "w:gz") as app_tarball:
126
126
  # Collect all wheels from both Python versions
127
127
  all_wheels = set(
128
- app_meta.pip39_dependencies.wheel + app_meta.pip313_dependencies.wheel
128
+ app_meta.pip313_dependencies.wheel + app_meta.pip314_dependencies.wheel
129
129
  )
130
130
 
131
131
  # Run the async collection function within an event loop
@@ -188,8 +188,8 @@ def build(
188
188
  input_file=wheel_archive_path,
189
189
  input_file_aarch64=wheel_archive_path,
190
190
  )
191
- app_meta.pip39_dependencies.wheel.append(wheel_entry)
192
191
  app_meta.pip313_dependencies.wheel.append(wheel_entry)
192
+ app_meta.pip314_dependencies.wheel.append(wheel_entry)
193
193
 
194
194
  console.print("Writing manifest")
195
195
  manifest_json = json.dumps(app_meta.to_json_manifest(), indent=4).encode()
@@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator
6
6
  @asynccontextmanager
7
7
  async def phantom_get_login_session(
8
8
  base_url: str, username: str, password: str
9
- ) -> AsyncGenerator[httpx.AsyncClient, None]:
9
+ ) -> AsyncGenerator[httpx.AsyncClient]:
10
10
  """Contextmanager that creates an authenticated client with CSRF token handling."""
11
11
  # Set longer timeouts for large file uploads
12
12
  timeout = httpx.Timeout(30.0, read=60.0)
@@ -1,14 +1,15 @@
1
- from typing import ClassVar, Optional
1
+ from typing import ClassVar
2
2
  from collections.abc import Iterator
3
3
  import typing
4
4
  import ast
5
- from pydantic.fields import Undefined
5
+ from pydantic_core import PydanticUndefined
6
6
 
7
7
  from soar_sdk.action_results import ActionOutput
8
8
  from soar_sdk.cli.utils import normalize_field_name
9
9
  from soar_sdk.code_renderers.renderer import AstRenderer
10
10
  from soar_sdk.meta.actions import ActionMeta
11
11
  from soar_sdk.params import Params
12
+ from soar_sdk.field_utils import parse_json_schema_extra
12
13
 
13
14
 
14
15
  class ActionRenderer(AstRenderer[ActionMeta]):
@@ -201,7 +202,7 @@ class ActionRenderer(AstRenderer[ActionMeta]):
201
202
  yield ast.fix_missing_locations(node)
202
203
 
203
204
  def render_outputs_ast(
204
- self, model: Optional[type[ActionOutput]] = None
205
+ self, model: type[ActionOutput] | None = None
205
206
  ) -> Iterator[ast.ClassDef]:
206
207
  """Generates the AST for the action outputs.
207
208
 
@@ -221,16 +222,24 @@ class ActionRenderer(AstRenderer[ActionMeta]):
221
222
 
222
223
  field_defs: list[ast.stmt] = []
223
224
 
224
- for field_name_str, field in model.__fields__.items():
225
+ for field_name_str, field in model.model_fields.items():
225
226
  annotation = field.annotation
227
+ if annotation is None:
228
+ continue
229
+
226
230
  annotation_str = "{name}"
227
231
  while typing.get_origin(annotation) is list:
228
232
  annotation_str = f"list[{annotation_str}]"
229
233
  annotation = typing.get_args(annotation)[0]
234
+
235
+ # Ensure annotation is a valid type after unwrapping
236
+ if not isinstance(annotation, type):
237
+ continue
238
+
230
239
  annotation_str = annotation_str.format(name=annotation.__name__)
231
240
 
232
241
  field_name = normalize_field_name(field_name_str)
233
- if field.alias != field_name.normalized:
242
+ if field.alias is not None and field.alias != field_name.normalized:
234
243
  field_name.original = field.alias
235
244
  field_name.modified = True
236
245
 
@@ -240,9 +249,10 @@ class ActionRenderer(AstRenderer[ActionMeta]):
240
249
  simple=1,
241
250
  )
242
251
 
243
- if issubclass(annotation, ActionOutput):
252
+ if isinstance(annotation, type) and issubclass(annotation, ActionOutput):
244
253
  # If the field is a Pydantic model, recursively print its fields
245
- for model_ast in self.render_outputs_ast(field.type_):
254
+ # In Pydantic v2, use annotation directly (no field.type_)
255
+ for model_ast in self.render_outputs_ast(annotation):
246
256
  model_tree[model_ast.name] = model_ast
247
257
 
248
258
  if field_name.modified:
@@ -258,7 +268,9 @@ class ActionRenderer(AstRenderer[ActionMeta]):
258
268
  )
259
269
  else:
260
270
  keywords = []
261
- if (extras := {**field.field_info.extra}) or field_name.modified:
271
+ extras = {**parse_json_schema_extra(field.json_schema_extra)}
272
+
273
+ if extras or field_name.modified:
262
274
  extras["example_values"] = extras.pop("examples", None)
263
275
  if extras["example_values"] == [True, False]:
264
276
  extras["example_values"] = None
@@ -315,7 +327,10 @@ class ActionRenderer(AstRenderer[ActionMeta]):
315
327
  keywords=[],
316
328
  )
317
329
 
318
- for field_name, field_def in self.action_meta.parameters.__fields__.items():
330
+ for field_name, field_def in self.action_meta.parameters.model_fields.items():
331
+ if field_def.annotation is None:
332
+ continue
333
+
319
334
  field_type = ast.Name(id=field_def.annotation.__name__, ctx=ast.Load())
320
335
 
321
336
  param = ast.Call(
@@ -324,29 +339,31 @@ class ActionRenderer(AstRenderer[ActionMeta]):
324
339
  keywords=[],
325
340
  )
326
341
 
327
- if field_def.field_info.description:
342
+ json_schema_extra = parse_json_schema_extra(field_def.json_schema_extra)
343
+
344
+ if field_def.description:
328
345
  param.keywords.append(
329
346
  ast.keyword(
330
347
  arg="description",
331
- value=ast.Constant(value=field_def.field_info.description),
348
+ value=ast.Constant(value=field_def.description),
332
349
  )
333
350
  )
334
- if not field_def.field_info.extra.get("required", True):
351
+ if not json_schema_extra.get("required", True):
335
352
  param.keywords.append(
336
353
  ast.keyword(arg="required", value=ast.Constant(value=False))
337
354
  )
338
- if field_def.field_info.extra.get("primary", False):
355
+ if json_schema_extra.get("primary", False):
339
356
  param.keywords.append(
340
357
  ast.keyword(arg="primary", value=ast.Constant(value=True))
341
358
  )
342
- if (default := field_def.field_info.default) and default != Undefined:
359
+ if field_def.default not in (PydanticUndefined, None):
343
360
  param.keywords.append(
344
361
  ast.keyword(
345
362
  arg="default",
346
- value=ast.Constant(value=field_def.field_info.default),
363
+ value=ast.Constant(value=field_def.default),
347
364
  )
348
365
  )
349
- if value_list := field_def.field_info.extra.get("value_list"):
366
+ if value_list := json_schema_extra.get("value_list"):
350
367
  param.keywords.append(
351
368
  ast.keyword(
352
369
  arg="value_list",
@@ -356,7 +373,7 @@ class ActionRenderer(AstRenderer[ActionMeta]):
356
373
  ),
357
374
  )
358
375
  )
359
- if cef_types := field_def.field_info.extra.get("cef_types"):
376
+ if cef_types := json_schema_extra.get("cef_types"):
360
377
  param.keywords.append(
361
378
  ast.keyword(
362
379
  arg="cef_types",
@@ -366,7 +383,7 @@ class ActionRenderer(AstRenderer[ActionMeta]):
366
383
  ),
367
384
  )
368
385
  )
369
- if field_def.field_info.extra.get("allow_list", False):
386
+ if json_schema_extra.get("allow_list", False):
370
387
  param.keywords.append(
371
388
  ast.keyword(arg="allow_list", value=ast.Constant(value=True))
372
389
  )
@@ -1,6 +1,5 @@
1
1
  import dataclasses
2
2
  import ast
3
- from typing import Union
4
3
  from collections.abc import Iterator
5
4
 
6
5
 
@@ -50,7 +49,7 @@ class AppRenderer:
50
49
  self.context = context
51
50
 
52
51
  @staticmethod
53
- def create_default_imports() -> Iterator[Union[ast.Import, ast.ImportFrom]]:
52
+ def create_default_imports() -> Iterator[ast.Import | ast.ImportFrom]:
54
53
  """Create default imports for the App module.
55
54
 
56
55
  Returns:
@@ -1,5 +1,4 @@
1
1
  import dataclasses
2
- from typing import Optional, Union
3
2
  import ast
4
3
  from collections.abc import Iterator
5
4
 
@@ -12,12 +11,12 @@ class AssetContext:
12
11
  """Context for rendering individual configuration keys of an Asset class."""
13
12
 
14
13
  name: str
15
- description: Optional[str]
14
+ description: str | None
16
15
  required: bool
17
- default: Union[str, int, float, bool, None]
16
+ default: str | int | float | bool | None
18
17
  data_type: str
19
- value_list: Optional[list[str]]
20
- alias: Optional[str] = None
18
+ value_list: list[str] | None
19
+ alias: str | None = None
21
20
 
22
21
  @property
23
22
  def is_str(self) -> bool:
@@ -1,5 +1,5 @@
1
1
  import abc
2
- from typing import TypeVar, Generic, Optional
2
+ from typing import TypeVar, Generic
3
3
  import jinja2 as j2
4
4
  import ast
5
5
  from collections.abc import Iterator
@@ -12,7 +12,7 @@ class Renderer(Generic[ContextT], abc.ABC):
12
12
  """Abstract base class for rendering code using Jinja2 templates."""
13
13
 
14
14
  def __init__(
15
- self, context: ContextT, jinja_env: Optional[j2.Environment] = None
15
+ self, context: ContextT, jinja_env: j2.Environment | None = None
16
16
  ) -> None:
17
17
  self.context = context
18
18
  self.jinja_env = jinja_env or j2.Environment(
@@ -52,7 +52,7 @@ required-environments = [
52
52
  [tool.ruff]
53
53
  output-format = "full" # <full|concise>
54
54
  fix = true
55
- target-version = "py39"
55
+ target-version = "py313"
56
56
 
57
57
  [tool.ruff.lint]
58
58
  select = [
soar_sdk/compat.py CHANGED
@@ -2,7 +2,7 @@ from enum import Enum
2
2
  import functools
3
3
  from packaging.version import Version
4
4
 
5
- MIN_PHANTOM_VERSION = "6.4.0"
5
+ MIN_PHANTOM_VERSION = "7.0.0"
6
6
 
7
7
  UPDATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
8
8
 
@@ -27,8 +27,8 @@ def remove_when_soar_newer_than(
27
27
  class PythonVersion(str, Enum):
28
28
  """Enum to represent supported Python versions."""
29
29
 
30
- PY_3_9 = "3.9"
31
30
  PY_3_13 = "3.13"
31
+ PY_3_14 = "3.14"
32
32
 
33
33
  def __str__(self) -> str:
34
34
  """Returns the string representation of the Python version."""
@@ -41,10 +41,10 @@ class PythonVersion(str, Enum):
41
41
  Raises ValueError if the version is not supported.
42
42
  """
43
43
  # "3" is a special case for connectors that don't properly define their Python version
44
- if version_str in ("3", "3.9"):
45
- return cls.PY_3_9
46
- if version_str == "3.13":
44
+ if version_str in ("3", "3.13"):
47
45
  return cls.PY_3_13
46
+ if version_str == "3.14":
47
+ return cls.PY_3_14
48
48
 
49
49
  raise ValueError(f"Unsupported Python version: {version_str}")
50
50
 
@@ -67,7 +67,7 @@ class PythonVersion(str, Enum):
67
67
  @classmethod
68
68
  def all(cls) -> list["PythonVersion"]:
69
69
  """Returns a list of all supported Python versions."""
70
- return [cls.PY_3_9, cls.PY_3_13]
70
+ return [cls.PY_3_13, cls.PY_3_14]
71
71
 
72
72
  @classmethod
73
73
  def all_csv(cls) -> str:
@@ -1,7 +1,8 @@
1
1
  import inspect
2
2
  from functools import wraps
3
3
  from collections.abc import Iterator
4
- from typing import Callable, Optional, Any, Union, get_args, get_origin
4
+ from typing import Any, get_args, get_origin
5
+ from collections.abc import Callable
5
6
  from collections.abc import AsyncGenerator
6
7
 
7
8
  from soar_sdk.abstract import SOARClient
@@ -25,24 +26,22 @@ class ActionDecorator:
25
26
  def __init__(
26
27
  self,
27
28
  app: "App",
28
- name: Optional[str] = None,
29
- identifier: Optional[str] = None,
30
- description: Optional[str] = None,
29
+ name: str | None = None,
30
+ identifier: str | None = None,
31
+ description: str | None = None,
31
32
  verbose: str = "",
32
33
  action_type: str = "generic",
33
34
  read_only: bool = True,
34
- params_class: Optional[type[Params]] = None,
35
- output_class: Union[
36
- None,
37
- type[ActionOutput],
38
- Iterator[type[ActionOutput]],
39
- AsyncGenerator[type[ActionOutput]],
40
- list[type[ActionOutput]],
41
- ] = None,
42
- render_as: Optional[str] = None,
43
- view_handler: Optional[Callable] = None,
35
+ params_class: type[Params] | None = None,
36
+ output_class: None
37
+ | type[ActionOutput]
38
+ | Iterator[type[ActionOutput]]
39
+ | AsyncGenerator[type[ActionOutput]]
40
+ | list[type[ActionOutput]] = None,
41
+ render_as: str | None = None,
42
+ view_handler: Callable | None = None,
44
43
  versions: str = "EQ(*)",
45
- summary_type: Optional[type[ActionOutput]] = None,
44
+ summary_type: type[ActionOutput] | None = None,
46
45
  enable_concurrency_lock: bool = False,
47
46
  ) -> None:
48
47
  self.app = app
@@ -12,7 +12,8 @@ from soar_sdk.logging import getLogger
12
12
  from functools import wraps
13
13
  import traceback
14
14
 
15
- from typing import TYPE_CHECKING, Callable, Any, Optional
15
+ from typing import TYPE_CHECKING, Any
16
+ from collections.abc import Callable
16
17
 
17
18
  if TYPE_CHECKING:
18
19
  from soar_sdk.app import App
@@ -24,7 +25,7 @@ class MakeRequestDecorator:
24
25
  def __init__(
25
26
  self,
26
27
  app: "App",
27
- output_class: Optional[type[ActionOutput]] = None,
28
+ output_class: type[ActionOutput] | None = None,
28
29
  ) -> None:
29
30
  self.app = app
30
31
  self.output_class = output_class
@@ -97,7 +98,7 @@ class MakeRequestDecorator:
97
98
  **kwargs: Any, # noqa: ANN401
98
99
  ) -> bool:
99
100
  try:
100
- action_params = validated_params_class.parse_obj(params)
101
+ action_params = validated_params_class.model_validate(params)
101
102
  except Exception as e:
102
103
  logger.info(f"Parameter validation error: {e!s}")
103
104
  return self.app._adapt_action_result(
@@ -1,6 +1,7 @@
1
1
  import inspect
2
2
  from functools import wraps
3
- from typing import Callable, Any
3
+ from typing import Any
4
+ from collections.abc import Callable
4
5
  from collections.abc import Iterator
5
6
 
6
7
  from soar_sdk.abstract import SOARClient
@@ -80,7 +81,7 @@ class OnPollDecorator:
80
81
  try:
81
82
  # Validate poll params
82
83
  try:
83
- action_params = validated_params_class.parse_obj(params)
84
+ action_params = validated_params_class.model_validate(params)
84
85
  except Exception as e:
85
86
  logger.info(f"Parameter validation error: {e!s}")
86
87
  return self.app._adapt_action_result(
@@ -177,8 +178,8 @@ class OnPollDecorator:
177
178
 
178
179
  # Custom ActionMeta class for on_poll (has no output)
179
180
  class OnPollActionMeta(ActionMeta):
180
- def dict(self, *args: object, **kwargs: object) -> dict[str, Any]:
181
- data = super().dict(*args, **kwargs)
181
+ def model_dump(self, *args: object, **kwargs: object) -> dict[str, Any]:
182
+ data = super().model_dump(*args, **kwargs)
182
183
  # Poll actions have no output
183
184
  data["output"] = []
184
185
  return data
@@ -1,6 +1,6 @@
1
1
  import inspect
2
2
  from functools import wraps
3
- from typing import Callable, Optional
3
+ from collections.abc import Callable
4
4
 
5
5
  from soar_sdk.abstract import SOARClient
6
6
  from soar_sdk.action_results import ActionResult
@@ -48,7 +48,7 @@ class ConnectivityTestDecorator:
48
48
  @action_protocol
49
49
  @wraps(function)
50
50
  def inner(
51
- _param: Optional[dict] = None,
51
+ _param: dict | None = None,
52
52
  soar: SOARClient = self.app.soar_client,
53
53
  ) -> bool:
54
54
  kwargs = self.app._build_magic_args(function, soar=soar)