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
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()
@@ -3,7 +3,7 @@ import inspect
3
3
  import json
4
4
  from pathlib import Path
5
5
  import typing
6
- from typing import Optional, Any
6
+ from typing import Any
7
7
  import os
8
8
  from pydantic import ValidationError
9
9
  from urllib.parse import urlparse, parse_qs
@@ -31,7 +31,7 @@ class AppCliRunner:
31
31
  def __init__(self, app: "App") -> None:
32
32
  self.app = app
33
33
 
34
- def parse_args(self, argv: Optional[list[str]] = None) -> argparse.Namespace:
34
+ def parse_args(self, argv: list[str] | None = None) -> argparse.Namespace:
35
35
  """Parse command line arguments for the app CLI runner."""
36
36
  root_parser = argparse.ArgumentParser()
37
37
  root_parser.add_argument(
@@ -162,13 +162,13 @@ class AppCliRunner:
162
162
  )
163
163
 
164
164
  try:
165
- param = chosen_action.params_class.parse_obj(params_json)
165
+ param = chosen_action.params_class.model_validate(params_json)
166
166
  except Exception as e:
167
167
  root_parser.error(
168
168
  f"Unable to parse parameter JSON file {params_file}:\n{e}"
169
169
  )
170
170
 
171
- parameter_list.append(ActionParameter(**param.dict()))
171
+ parameter_list.append(ActionParameter(**param.model_dump()))
172
172
 
173
173
  input_data = InputSpecification(
174
174
  action=args.identifier,
@@ -191,7 +191,7 @@ class AppCliRunner:
191
191
  )
192
192
 
193
193
  input_data.config = AppConfig(
194
- **input_data.config.dict(),
194
+ **input_data.config.model_dump(),
195
195
  **asset_json, # Merge asset JSON into config
196
196
  )
197
197
 
@@ -207,7 +207,7 @@ class AppCliRunner:
207
207
  except ValidationError as e:
208
208
  root_parser.error(f"Provided soar auth arguments are invalid: {e}.")
209
209
 
210
- args.raw_input_data = input_data.json()
210
+ args.raw_input_data = input_data.model_dump_json()
211
211
 
212
212
  def _parse_webhook_args(
213
213
  self,
@@ -250,14 +250,14 @@ class AppCliRunner:
250
250
  path_parts=path_parts,
251
251
  query=query,
252
252
  body=args.data,
253
- asset=self.app.asset_cls.parse_obj(asset_json),
253
+ asset=self.app.asset_cls.model_validate(asset_json),
254
254
  soar_base_url=soar_base_url,
255
255
  soar_auth_token=soar_auth_token,
256
256
  asset_id=args.asset_id,
257
257
  )
258
258
  print(f"Parsed webhook request: {args.webhook_request}")
259
259
 
260
- def run(self, argv: Optional[list[str]] = None) -> None:
260
+ def run(self, argv: list[str] | None = None) -> None:
261
261
  """Run the app CLI."""
262
262
  args = self.parse_args(argv=argv)
263
263
 
soar_sdk/app_client.py CHANGED
@@ -1,5 +1,5 @@
1
1
  from dataclasses import dataclass
2
- from typing import Any, TYPE_CHECKING, Union, Optional
2
+ from typing import Any, TYPE_CHECKING
3
3
  from collections.abc import Mapping
4
4
 
5
5
  import httpx
@@ -41,9 +41,9 @@ class AppClient(SOARClient[SummaryType]):
41
41
  self._artifacts_api = Artifact(soar_client=self)
42
42
  self._containers_api = Container(soar_client=self)
43
43
  self._vault_api = Vault(soar_client=self)
44
- self.basic_auth: Optional[BasicAuth] = None
44
+ self.basic_auth: BasicAuth | None = None
45
45
 
46
- self._summary: Optional[SummaryType] = None
46
+ self._summary: SummaryType | None = None
47
47
  self._message: str = ""
48
48
  self.__container_id: int = 0
49
49
  self.__asset_id: str = ""
@@ -132,13 +132,13 @@ class AppClient(SOARClient[SummaryType]):
132
132
  self,
133
133
  endpoint: str,
134
134
  *,
135
- params: Optional[Union[dict[str, Any], httpx.QueryParams]] = None,
136
- headers: Optional[dict[str, str]] = None,
137
- cookies: Optional[dict[str, str]] = None,
138
- auth: Optional[Union[httpx.Auth, tuple[str, str]]] = None,
139
- timeout: Optional[httpx.Timeout] = None,
135
+ params: dict[str, Any] | httpx.QueryParams | None = None,
136
+ headers: dict[str, str] | None = None,
137
+ cookies: dict[str, str] | None = None,
138
+ auth: httpx.Auth | tuple[str, str] | None = None,
139
+ timeout: httpx.Timeout | None = None,
140
140
  follow_redirects: bool = False,
141
- extensions: Optional[Mapping[str, Any]] = None,
141
+ extensions: Mapping[str, Any] | None = None,
142
142
  ) -> httpx.Response:
143
143
  """Perform a DELETE request to the specific endpoint using the SOAR client."""
144
144
  headers = headers or {}
@@ -167,7 +167,7 @@ class AppClient(SOARClient[SummaryType]):
167
167
  """Set the message for the action result."""
168
168
  self._message = message
169
169
 
170
- def get_summary(self) -> Optional[SummaryType]:
170
+ def get_summary(self) -> SummaryType | None:
171
171
  """Get the summary for the action result."""
172
172
  return self._summary
173
173
 
soar_sdk/asset.py CHANGED
@@ -1,14 +1,16 @@
1
- from typing import Any, Optional, Union
1
+ from typing import Any
2
2
  from zoneinfo import ZoneInfo
3
- from pydantic import BaseModel, root_validator
4
- from pydantic.fields import Field, Undefined
3
+ from pydantic import BaseModel, model_validator, ConfigDict, Field
4
+ from pydantic_core import PydanticUndefined
5
5
 
6
- from typing_extensions import NotRequired, TypedDict
6
+ from typing_extensions import TypedDict
7
+ from typing import NotRequired
7
8
 
8
9
 
9
10
  from soar_sdk.compat import remove_when_soar_newer_than
10
11
  from soar_sdk.meta.datatypes import as_datatype
11
12
  from soar_sdk.input_spec import AppConfig
13
+ from soar_sdk.field_utils import parse_json_schema_extra
12
14
 
13
15
  remove_when_soar_newer_than(
14
16
  "7.0.0", "NotRequired from typing_extensions is in typing in Python 3.11+"
@@ -16,12 +18,12 @@ remove_when_soar_newer_than(
16
18
 
17
19
 
18
20
  def AssetField(
19
- description: Optional[str] = None,
21
+ description: str | None = None,
20
22
  required: bool = True,
21
- default: Optional[Any] = None, # noqa: ANN401
22
- value_list: Optional[list] = None,
23
+ default: Any | None = None, # noqa: ANN401
24
+ value_list: list | None = None,
23
25
  sensitive: bool = False,
24
- alias: Optional[str] = None,
26
+ alias: str | None = None,
25
27
  ) -> Any: # noqa: ANN401
26
28
  """Representation of an asset configuration field.
27
29
 
@@ -42,13 +44,22 @@ def AssetField(
42
44
  Returns:
43
45
  The FieldInfo object as pydantic.Field.
44
46
  """
47
+ json_schema_extra: dict[str, Any] = {}
48
+ if required is not None:
49
+ json_schema_extra["required"] = required
50
+ if value_list is not None:
51
+ json_schema_extra["value_list"] = value_list
52
+ if sensitive is not None:
53
+ json_schema_extra["sensitive"] = sensitive
54
+
55
+ # Use ... for required fields
56
+ field_default: Any = ... if default is None and required else default
57
+
45
58
  return Field(
46
- default=default,
59
+ default=field_default,
47
60
  description=description,
48
- required=required,
49
- value_list=value_list,
50
- sensitive=sensitive,
51
61
  alias=alias,
62
+ json_schema_extra=json_schema_extra if json_schema_extra else None,
52
63
  )
53
64
 
54
65
 
@@ -71,7 +82,7 @@ class AssetFieldSpecification(TypedDict):
71
82
  data_type: str
72
83
  description: NotRequired[str]
73
84
  required: NotRequired[bool]
74
- default: NotRequired[Union[str, int, float, bool]]
85
+ default: NotRequired[str | int | float | bool]
75
86
  value_list: NotRequired[list[str]]
76
87
  order: NotRequired[int]
77
88
 
@@ -106,16 +117,12 @@ class BaseAsset(BaseModel):
106
117
  the SOAR platform to avoid conflicts with internal fields.
107
118
  """
108
119
 
109
- class Config:
110
- """Pydantic configuration for BaseAsset.
111
-
112
- Note that we are using the `arbitrary_types_allowed` setting, which is generally not recommended.
113
- However, we are checking all of the field types via `soar_sdk.datatypes.as_datatype`, so we have confidence in their validity.
114
- """
115
-
116
- arbitrary_types_allowed = True
120
+ model_config = ConfigDict(
121
+ arbitrary_types_allowed=True,
122
+ )
117
123
 
118
- @root_validator(pre=True)
124
+ @model_validator(mode="before")
125
+ @classmethod
119
126
  def validate_no_reserved_fields(cls, values: dict[str, Any]) -> dict[str, Any]:
120
127
  """Prevents subclasses from defining fields starting with "_reserved_".
121
128
 
@@ -148,7 +155,7 @@ class BaseAsset(BaseModel):
148
155
  # This accounts for some bad behavior by the platform; it injects a few app-related
149
156
  # metadata fields directly into asset configuration dictionaries, which can lead to
150
157
  # undefined behavior if an asset tries to use the same field names.
151
- if field_name in AppConfig.__fields__:
158
+ if field_name in AppConfig.model_fields:
152
159
  raise ValueError(
153
160
  f"Field name '{field_name}' is reserved by the platform and cannot be used in an asset"
154
161
  )
@@ -206,8 +213,10 @@ class BaseAsset(BaseModel):
206
213
  """
207
214
  params: dict[str, AssetFieldSpecification] = {}
208
215
 
209
- for field_order, (field_name, field) in enumerate(cls.__fields__.items()):
216
+ for field_order, (field_name, field) in enumerate(cls.model_fields.items()):
210
217
  field_type = field.annotation
218
+ if field_type is None:
219
+ continue
211
220
 
212
221
  try:
213
222
  type_name = as_datatype(field_type)
@@ -216,32 +225,34 @@ class BaseAsset(BaseModel):
216
225
  f"Failed to serialize asset field {field_name}: {e}"
217
226
  ) from None
218
227
 
219
- if field.field_info.extra.get("sensitive", False):
228
+ json_schema_extra = parse_json_schema_extra(field.json_schema_extra)
229
+
230
+ if json_schema_extra.get("sensitive", False):
220
231
  if field_type is not str:
221
232
  raise TypeError(
222
233
  f"Sensitive parameter {field_name} must be type str, not {field_type.__name__}"
223
234
  )
224
235
  type_name = "password"
225
236
 
226
- if not (description := field.field_info.description):
237
+ if not (description := field.description):
227
238
  description = cls._default_field_description(field_name)
228
239
 
229
240
  params_field = AssetFieldSpecification(
230
241
  data_type=type_name,
231
- required=field.field_info.extra.get("required", True),
242
+ required=bool(json_schema_extra.get("required", True)),
232
243
  description=description,
233
244
  order=field_order,
234
245
  )
235
246
 
236
- if (default := field.field_info.default) and default != Undefined:
247
+ if (default := field.default) not in (PydanticUndefined, None):
237
248
  if isinstance(default, ZoneInfo):
238
249
  params_field["default"] = default.key
239
250
  else:
240
251
  params_field["default"] = default
241
- if value_list := field.field_info.extra.get("value_list"):
252
+ if value_list := json_schema_extra.get("value_list"):
242
253
  params_field["value_list"] = value_list
243
254
 
244
- params[field.alias] = params_field
255
+ params[field.alias or field_name] = params_field
245
256
 
246
257
  return params
247
258
 
@@ -255,8 +266,9 @@ class BaseAsset(BaseModel):
255
266
  """
256
267
  return {
257
268
  field_name
258
- for field_name, field in cls.__fields__.items()
259
- if field.field_info.extra.get("sensitive", False)
269
+ for field_name, field in cls.model_fields.items()
270
+ if isinstance(field.json_schema_extra, dict)
271
+ and field.json_schema_extra.get("sensitive", False)
260
272
  }
261
273
 
262
274
  @classmethod
@@ -268,6 +280,6 @@ class BaseAsset(BaseModel):
268
280
  """
269
281
  return {
270
282
  field_name
271
- for field_name, field in cls.__fields__.items()
283
+ for field_name, field in cls.model_fields.items()
272
284
  if field.annotation is ZoneInfo
273
285
  }
soar_sdk/async_utils.py CHANGED
@@ -17,7 +17,7 @@ def is_async_generator(obj: Any) -> bool: # noqa: ANN401
17
17
  return inspect.isasyncgen(obj)
18
18
 
19
19
 
20
- async def async_generator_to_list(agen: AsyncGenerator[T, None]) -> list[T]:
20
+ async def async_generator_to_list(agen: AsyncGenerator[T]) -> list[T]:
21
21
  """Consume an async generator and return its items in a list."""
22
22
  result: list[T] = []
23
23
  # Python 3.9 coverage limitation with async for loops
@@ -31,7 +31,7 @@ def run_async_if_needed(result: Coroutine[Any, Any, T]) -> T: ...
31
31
 
32
32
 
33
33
  @overload
34
- def run_async_if_needed(result: AsyncGenerator[T, None]) -> list[T]: ...
34
+ def run_async_if_needed(result: AsyncGenerator[T]) -> list[T]: ...
35
35
 
36
36
 
37
37
  @overload
soar_sdk/cli/init/cli.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Annotated, Optional, cast
1
+ from typing import Annotated, cast
2
2
  import datetime
3
3
  import json
4
4
  import os
@@ -65,7 +65,7 @@ def init_callback(
65
65
  type: str = "generic", # noqa: A002
66
66
  vendor: str = "Splunk Inc.",
67
67
  publisher: str = "Splunk Inc.",
68
- product: Optional[str] = None,
68
+ product: str | None = None,
69
69
  fips_compliant: bool = False,
70
70
  overwrite: bool = False,
71
71
  ) -> None:
@@ -107,11 +107,11 @@ def init_sdk_app(
107
107
  publisher: str,
108
108
  logo: Path,
109
109
  logo_dark: Path,
110
- product: Optional[str] = None,
110
+ product: str | None = None,
111
111
  fips_compliant: bool = False,
112
112
  overwrite: bool = False,
113
- app_content: Optional[list[ast.stmt]] = None,
114
- asset_class: Optional[ast.ClassDef] = None,
113
+ app_content: list[ast.stmt] | None = None,
114
+ asset_class: ast.ClassDef | None = None,
115
115
  ) -> None:
116
116
  """Initialize a new SOAR app in the specified directory."""
117
117
  app_dir.mkdir(exist_ok=True)
@@ -149,9 +149,7 @@ def init_sdk_app(
149
149
  name=name,
150
150
  version=version,
151
151
  description=description,
152
- copyright=copyright.format(
153
- year=datetime.datetime.now(datetime.timezone.utc).year
154
- ),
152
+ copyright=copyright.format(year=datetime.datetime.now(datetime.UTC).year),
155
153
  python_versions=python_versions,
156
154
  authors=authors,
157
155
  dependencies=dependencies,
@@ -242,7 +240,7 @@ def convert_connector_to_sdk(
242
240
  ),
243
241
  ],
244
242
  output_dir: Annotated[
245
- Optional[Path],
243
+ Path | None,
246
244
  typer.Argument(
247
245
  file_okay=False,
248
246
  dir_okay=True,