linkedapi 1.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 (72) hide show
  1. linkedapi/__init__.py +127 -0
  2. linkedapi/admin/__init__.py +13 -0
  3. linkedapi/admin/accounts.py +91 -0
  4. linkedapi/admin/admin.py +22 -0
  5. linkedapi/admin/http_client.py +73 -0
  6. linkedapi/admin/limits.py +70 -0
  7. linkedapi/admin/subscription.py +69 -0
  8. linkedapi/client.py +160 -0
  9. linkedapi/config.py +10 -0
  10. linkedapi/core/__init__.py +4 -0
  11. linkedapi/core/operation.py +89 -0
  12. linkedapi/core/polling.py +48 -0
  13. linkedapi/errors.py +119 -0
  14. linkedapi/http/__init__.py +4 -0
  15. linkedapi/http/base.py +22 -0
  16. linkedapi/http/linked_api_http_client.py +74 -0
  17. linkedapi/mappers/__init__.py +16 -0
  18. linkedapi/mappers/array.py +49 -0
  19. linkedapi/mappers/base.py +70 -0
  20. linkedapi/mappers/simple.py +56 -0
  21. linkedapi/mappers/then.py +175 -0
  22. linkedapi/mappers/void.py +33 -0
  23. linkedapi/operations/__init__.py +58 -0
  24. linkedapi/operations/check_connection_status.py +15 -0
  25. linkedapi/operations/comment_on_post.py +12 -0
  26. linkedapi/operations/create_post.py +15 -0
  27. linkedapi/operations/custom_workflow.py +22 -0
  28. linkedapi/operations/fetch_company.py +35 -0
  29. linkedapi/operations/fetch_person.py +43 -0
  30. linkedapi/operations/fetch_post.py +33 -0
  31. linkedapi/operations/nv_fetch_company.py +33 -0
  32. linkedapi/operations/nv_fetch_person.py +23 -0
  33. linkedapi/operations/nv_search_companies.py +15 -0
  34. linkedapi/operations/nv_search_people.py +15 -0
  35. linkedapi/operations/nv_send_message.py +12 -0
  36. linkedapi/operations/nv_sync_conversation.py +12 -0
  37. linkedapi/operations/react_to_post.py +12 -0
  38. linkedapi/operations/remove_connection.py +12 -0
  39. linkedapi/operations/retrieve_connections.py +15 -0
  40. linkedapi/operations/retrieve_pending_requests.py +15 -0
  41. linkedapi/operations/retrieve_performance.py +15 -0
  42. linkedapi/operations/retrieve_ssi.py +15 -0
  43. linkedapi/operations/search_companies.py +15 -0
  44. linkedapi/operations/search_people.py +15 -0
  45. linkedapi/operations/send_connection_request.py +12 -0
  46. linkedapi/operations/send_message.py +12 -0
  47. linkedapi/operations/sync_conversation.py +12 -0
  48. linkedapi/operations/withdraw_connection_request.py +12 -0
  49. linkedapi/py.typed +1 -0
  50. linkedapi/types/__init__.py +336 -0
  51. linkedapi/types/account.py +8 -0
  52. linkedapi/types/admin/__init__.py +91 -0
  53. linkedapi/types/admin/accounts.py +71 -0
  54. linkedapi/types/admin/config.py +8 -0
  55. linkedapi/types/admin/limits.py +77 -0
  56. linkedapi/types/admin/subscription.py +58 -0
  57. linkedapi/types/base.py +47 -0
  58. linkedapi/types/company.py +140 -0
  59. linkedapi/types/connection.py +86 -0
  60. linkedapi/types/message.py +48 -0
  61. linkedapi/types/params.py +15 -0
  62. linkedapi/types/person.py +105 -0
  63. linkedapi/types/post.py +119 -0
  64. linkedapi/types/responses.py +18 -0
  65. linkedapi/types/search_companies.py +72 -0
  66. linkedapi/types/search_people.py +46 -0
  67. linkedapi/types/statistics.py +27 -0
  68. linkedapi/types/workflow.py +55 -0
  69. linkedapi-1.0.0.dist-info/METADATA +125 -0
  70. linkedapi-1.0.0.dist-info/RECORD +72 -0
  71. linkedapi-1.0.0.dist-info/WHEEL +4 -0
  72. linkedapi-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC
4
+ from typing import Any, Generic, TypeVar
5
+
6
+ from linkedapi.core.polling import poll_workflow_result
7
+ from linkedapi.errors import LinkedApiError, LinkedApiWorkflowTimeoutError
8
+ from linkedapi.http import HttpClient
9
+ from linkedapi.mappers import BaseMapper, MappedResponse
10
+ from linkedapi.types import (
11
+ WorkflowCancelResponse,
12
+ WorkflowCompletion,
13
+ WorkflowInProgressResponse,
14
+ WorkflowResponse,
15
+ WorkflowStartedResponse,
16
+ )
17
+
18
+ TParams = TypeVar("TParams")
19
+ TResult = TypeVar("TResult")
20
+
21
+
22
+ class Operation(ABC, Generic[TParams, TResult]):
23
+ operation_name: str
24
+ mapper: BaseMapper[TParams, TResult]
25
+
26
+ def __init__(self, http_client: HttpClient[Any]) -> None:
27
+ self.http_client = http_client
28
+
29
+ def execute(self, params: TParams | None = None) -> WorkflowStartedResponse:
30
+ request = self.mapper.map_request(params)
31
+ response = self.http_client.post("/workflows", request)
32
+ if response.error:
33
+ raise LinkedApiError(response.error.type, response.error.message)
34
+ if response.result is None:
35
+ raise LinkedApiError.unknown_error()
36
+ return WorkflowStartedResponse.model_validate(response.result)
37
+
38
+ def result(
39
+ self,
40
+ workflow_id: str,
41
+ *,
42
+ poll_interval: float | None = None,
43
+ timeout: float | None = None,
44
+ ) -> MappedResponse[TResult]:
45
+ try:
46
+ return poll_workflow_result(
47
+ lambda: self.status(workflow_id),
48
+ poll_interval=5.0 if poll_interval is None else poll_interval,
49
+ timeout=86400.0 if timeout is None else timeout,
50
+ )
51
+ except LinkedApiError as error:
52
+ if error.type == "workflowTimeout":
53
+ raise LinkedApiWorkflowTimeoutError(workflow_id, self.operation_name) from error
54
+ raise
55
+
56
+ def status(self, workflow_id: str) -> WorkflowInProgressResponse | MappedResponse[TResult]:
57
+ workflow_result = self._get_workflow_result(workflow_id)
58
+ if workflow_result.workflow_status in {"running", "pending"}:
59
+ return WorkflowInProgressResponse(
60
+ workflow_id=workflow_id,
61
+ workflow_status=workflow_result.workflow_status,
62
+ message=workflow_result.message,
63
+ )
64
+
65
+ completion = self._get_completion(workflow_result)
66
+ return self.mapper.map_response(completion)
67
+
68
+ def cancel(self, workflow_id: str) -> bool:
69
+ response = self.http_client.delete(f"/workflows/{workflow_id}")
70
+ if response.error:
71
+ raise LinkedApiError(response.error.type, response.error.message)
72
+ if response.result is None:
73
+ raise LinkedApiError.unknown_error()
74
+ return WorkflowCancelResponse.model_validate(response.result).cancelled
75
+
76
+ def _get_workflow_result(self, workflow_id: str) -> WorkflowResponse:
77
+ response = self.http_client.get(f"/workflows/{workflow_id}")
78
+ if response.error:
79
+ raise LinkedApiError(response.error.type, response.error.message)
80
+ if response.result is None:
81
+ raise LinkedApiError.unknown_error()
82
+ return WorkflowResponse.model_validate(response.result)
83
+
84
+ def _get_completion(self, response: WorkflowResponse) -> WorkflowCompletion:
85
+ if response.completion is None:
86
+ if response.failure:
87
+ raise LinkedApiError(response.failure.reason, response.failure.message)
88
+ raise LinkedApiError.unknown_error()
89
+ return response.completion
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from collections.abc import Callable
5
+ from typing import TypeVar, cast
6
+
7
+ from linkedapi.errors import LinkedApiError
8
+ from linkedapi.types import WorkflowInProgressResponse
9
+
10
+ TResult = TypeVar("TResult")
11
+
12
+
13
+ def poll_workflow_result(
14
+ workflow_result_fn: Callable[[], TResult | WorkflowInProgressResponse],
15
+ *,
16
+ poll_interval: float = 5.0,
17
+ timeout: float = 86400.0,
18
+ max_invalid_attempts: int = 15,
19
+ ) -> TResult:
20
+ start_time = time.monotonic()
21
+ invalid_attempts = 0
22
+
23
+ while time.monotonic() - start_time < timeout:
24
+ try:
25
+ result = workflow_result_fn()
26
+ if not _is_workflow_in_progress(result):
27
+ return cast(TResult, result)
28
+ invalid_attempts = 0
29
+ except LinkedApiError as error:
30
+ if error.type == "httpError":
31
+ invalid_attempts += 1
32
+ if invalid_attempts > max_invalid_attempts:
33
+ raise
34
+ else:
35
+ raise
36
+
37
+ remaining = timeout - (time.monotonic() - start_time)
38
+ if remaining > 0:
39
+ time.sleep(min(poll_interval, remaining))
40
+
41
+ raise LinkedApiError("workflowTimeout", f"Workflow did not complete within {timeout}s")
42
+
43
+
44
+ def _is_workflow_in_progress(value: object) -> bool:
45
+ status = getattr(value, "workflow_status", None)
46
+ if isinstance(value, dict):
47
+ status = value.get("workflowStatus") or value.get("workflow_status")
48
+ return status in {"running", "pending"}
linkedapi/errors.py ADDED
@@ -0,0 +1,119 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Literal
4
+
5
+ LinkedApiActionErrorType = Literal[
6
+ "personNotFound",
7
+ "selfProfileNotAllowed",
8
+ "messagingNotAllowed",
9
+ "alreadyPending",
10
+ "alreadyConnected",
11
+ "emailRequired",
12
+ "requestNotAllowed",
13
+ "notPending",
14
+ "retrievingNotAllowed",
15
+ "connectionNotFound",
16
+ "searchingNotAllowed",
17
+ "companyNotFound",
18
+ "postNotFound",
19
+ "commentingNotAllowed",
20
+ "noPostingPermission",
21
+ "noSalesNavigator",
22
+ "conversationsNotSynced",
23
+ ]
24
+ LINKED_API_ACTION_ERROR_TYPES: tuple[str, ...] = (
25
+ "personNotFound",
26
+ "selfProfileNotAllowed",
27
+ "messagingNotAllowed",
28
+ "alreadyPending",
29
+ "alreadyConnected",
30
+ "emailRequired",
31
+ "requestNotAllowed",
32
+ "notPending",
33
+ "retrievingNotAllowed",
34
+ "connectionNotFound",
35
+ "searchingNotAllowed",
36
+ "companyNotFound",
37
+ "postNotFound",
38
+ "commentingNotAllowed",
39
+ "noPostingPermission",
40
+ "noSalesNavigator",
41
+ "conversationsNotSynced",
42
+ )
43
+
44
+ LinkedApiErrorType = Literal[
45
+ "linkedApiTokenRequired",
46
+ "invalidLinkedApiToken",
47
+ "identificationTokenRequired",
48
+ "invalidIdentificationToken",
49
+ "subscriptionRequired",
50
+ "invalidRequestPayload",
51
+ "invalidWorkflow",
52
+ "plusPlanRequired",
53
+ "linkedinAccountSignedOut",
54
+ "languageNotSupported",
55
+ "workflowTimeout",
56
+ "httpError",
57
+ "tooManyRequests",
58
+ "accountNotFound",
59
+ "accountIdRequired",
60
+ "sessionNotFound",
61
+ "noAvailableSeats",
62
+ "dailyConnectionAttemptsExceeded",
63
+ ]
64
+ LINKED_API_ERROR_TYPES: tuple[str, ...] = (
65
+ "linkedApiTokenRequired",
66
+ "invalidLinkedApiToken",
67
+ "identificationTokenRequired",
68
+ "invalidIdentificationToken",
69
+ "subscriptionRequired",
70
+ "invalidRequestPayload",
71
+ "invalidWorkflow",
72
+ "plusPlanRequired",
73
+ "linkedinAccountSignedOut",
74
+ "languageNotSupported",
75
+ "workflowTimeout",
76
+ "httpError",
77
+ "tooManyRequests",
78
+ "accountNotFound",
79
+ "accountIdRequired",
80
+ "sessionNotFound",
81
+ "noAvailableSeats",
82
+ "dailyConnectionAttemptsExceeded",
83
+ )
84
+
85
+
86
+ class LinkedApiError(Exception):
87
+ type: str
88
+ message: str
89
+ details: Any | None
90
+
91
+ def __init__(self, type: str, message: str, details: Any | None = None) -> None:
92
+ super().__init__(message)
93
+ self.type = type
94
+ self.message = message
95
+ self.details = details
96
+
97
+ @classmethod
98
+ def unknown_error(
99
+ cls, message: str = "Unknown error. Please contact support."
100
+ ) -> LinkedApiError:
101
+ return cls("unknownError", message)
102
+
103
+
104
+ class LinkedApiWorkflowTimeoutError(LinkedApiError):
105
+ workflow_id: str
106
+ operation_name: str
107
+
108
+ def __init__(self, workflow_id: str, operation_name: str) -> None:
109
+ message = (
110
+ f"Workflow {workflow_id} timed out. Call {operation_name}.result() again to continue "
111
+ "checking the workflow."
112
+ )
113
+ super().__init__(
114
+ "workflowTimeout",
115
+ message,
116
+ {"workflowId": workflow_id, "operationName": operation_name},
117
+ )
118
+ self.workflow_id = workflow_id
119
+ self.operation_name = operation_name
@@ -0,0 +1,4 @@
1
+ from linkedapi.http.base import HttpClient
2
+ from linkedapi.http.linked_api_http_client import LinkedApiHttpClient
3
+
4
+ __all__ = ["HttpClient", "LinkedApiHttpClient"]
linkedapi/http/base.py ADDED
@@ -0,0 +1,22 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Generic, TypeVar
5
+
6
+ from linkedapi.types.responses import LinkedApiResponse
7
+
8
+ TResult = TypeVar("TResult")
9
+
10
+
11
+ class HttpClient(ABC, Generic[TResult]):
12
+ @abstractmethod
13
+ def get(self, path: str) -> LinkedApiResponse[Any]:
14
+ raise NotImplementedError
15
+
16
+ @abstractmethod
17
+ def post(self, path: str, data: Any | None = None) -> LinkedApiResponse[Any]:
18
+ raise NotImplementedError
19
+
20
+ @abstractmethod
21
+ def delete(self, path: str) -> LinkedApiResponse[Any]:
22
+ raise NotImplementedError
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ import requests
6
+
7
+ from linkedapi.config import LinkedApiConfig
8
+ from linkedapi.errors import LinkedApiError
9
+ from linkedapi.http.base import HttpClient
10
+ from linkedapi.types import LinkedApiResponse, serialize_value
11
+
12
+
13
+ class LinkedApiHttpClient(HttpClient[Any]):
14
+ def __init__(
15
+ self,
16
+ config: LinkedApiConfig,
17
+ client: str | None = None,
18
+ base_url: str | None = None,
19
+ session: requests.Session | None = None,
20
+ ) -> None:
21
+ self.base_url = (base_url or config.base_url).rstrip("/")
22
+ self.session = session or requests.Session()
23
+ self.headers = {
24
+ "Content-Type": "application/json",
25
+ "linked-api-token": config.linked_api_token,
26
+ "identification-token": config.identification_token,
27
+ "client": client or config.client,
28
+ }
29
+
30
+ def get(self, path: str) -> LinkedApiResponse[Any]:
31
+ return self._request("GET", path)
32
+
33
+ def post(self, path: str, data: Any | None = None) -> LinkedApiResponse[Any]:
34
+ return self._request("POST", path, data)
35
+
36
+ def delete(self, path: str) -> LinkedApiResponse[Any]:
37
+ return self._request("DELETE", path)
38
+
39
+ def _request(self, method: str, path: str, data: Any | None = None) -> LinkedApiResponse[Any]:
40
+ try:
41
+ response = self.session.request(
42
+ method,
43
+ f"{self.base_url}{path}",
44
+ headers=self.headers,
45
+ json=serialize_value(data) if data is not None else None,
46
+ )
47
+ return self._handle_response(response)
48
+ except LinkedApiError:
49
+ raise
50
+ except requests.RequestException as error:
51
+ raise LinkedApiError(
52
+ "httpError", f"Request error: {error}", {"error": error}
53
+ ) from error
54
+
55
+ def _handle_response(self, response: requests.Response) -> LinkedApiResponse[Any]:
56
+ if response.ok:
57
+ return LinkedApiResponse[Any].model_validate(response.json())
58
+
59
+ try:
60
+ error_data = response.json()
61
+ error = error_data["error"]
62
+ raise LinkedApiError(error["type"], error["message"], error_data)
63
+ except LinkedApiError:
64
+ raise
65
+ except (KeyError, TypeError, ValueError) as error:
66
+ raise LinkedApiError(
67
+ "httpError",
68
+ f"HTTP {response.status_code}: {response.reason}",
69
+ {
70
+ "status": response.status_code,
71
+ "statusText": response.reason,
72
+ "url": response.url,
73
+ },
74
+ ) from error
@@ -0,0 +1,16 @@
1
+ from linkedapi.mappers.array import ArrayWorkflowMapper
2
+ from linkedapi.mappers.base import BaseMapper, MappedResponse
3
+ from linkedapi.mappers.simple import SimpleWorkflowMapper
4
+ from linkedapi.mappers.then import ActionConfig, ResponseMapping, ThenWorkflowMapper
5
+ from linkedapi.mappers.void import VoidWorkflowMapper
6
+
7
+ __all__ = [
8
+ "ActionConfig",
9
+ "ArrayWorkflowMapper",
10
+ "BaseMapper",
11
+ "MappedResponse",
12
+ "ResponseMapping",
13
+ "SimpleWorkflowMapper",
14
+ "ThenWorkflowMapper",
15
+ "VoidWorkflowMapper",
16
+ ]
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Generic, TypeVar, cast
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from linkedapi.mappers.base import (
8
+ BaseMapper,
9
+ MappedResponse,
10
+ as_action_dict,
11
+ collect_action_errors,
12
+ parse_action_error,
13
+ parse_result,
14
+ )
15
+ from linkedapi.types import WorkflowCompletion, WorkflowDefinition, serialize_model
16
+
17
+ TParams = TypeVar("TParams")
18
+ TItem = TypeVar("TItem")
19
+
20
+
21
+ class ArrayWorkflowMapper(BaseMapper[TParams, list[TItem]], Generic[TParams, TItem]):
22
+ def __init__(self, base_action_type: str, item_model: type[BaseModel] | None = None) -> None:
23
+ self.base_action_type = base_action_type
24
+ self.item_model = item_model
25
+
26
+ def map_request(self, params: TParams | None = None) -> WorkflowDefinition:
27
+ return {"actionType": self.base_action_type, **serialize_model(params)}
28
+
29
+ def map_response(self, completion: WorkflowCompletion) -> MappedResponse[list[TItem]]:
30
+ if isinstance(completion, list):
31
+ data = [as_action_dict(action).get("data") for action in completion]
32
+ errors = collect_action_errors(
33
+ [as_action_dict(action).get("error") for action in completion]
34
+ )
35
+ return MappedResponse(
36
+ data=cast(list[TItem], parse_result(data, self.item_model)), errors=errors
37
+ )
38
+
39
+ action = as_action_dict(completion)
40
+ error = parse_action_error(action.get("error"))
41
+ if error is not None:
42
+ return MappedResponse(data=None, errors=[error])
43
+
44
+ single_data = action.get("data")
45
+ data_list = single_data if isinstance(single_data, list) else [single_data]
46
+ return MappedResponse(
47
+ data=cast(list[TItem], parse_result(data_list, self.item_model)),
48
+ errors=[],
49
+ )
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Generic, TypeVar
6
+
7
+ from pydantic import BaseModel, ValidationError
8
+
9
+ from linkedapi.errors import LinkedApiError
10
+ from linkedapi.types import LinkedApiActionError, WorkflowCompletion, WorkflowDefinition
11
+
12
+ TParams = TypeVar("TParams")
13
+ TResult = TypeVar("TResult")
14
+
15
+
16
+ @dataclass
17
+ class MappedResponse(Generic[TResult]):
18
+ data: TResult | None = None
19
+ errors: list[LinkedApiActionError] = field(default_factory=list)
20
+
21
+
22
+ class BaseMapper(ABC, Generic[TParams, TResult]):
23
+ @abstractmethod
24
+ def map_request(self, params: TParams | None = None) -> WorkflowDefinition:
25
+ raise NotImplementedError
26
+
27
+ @abstractmethod
28
+ def map_response(self, completion: WorkflowCompletion) -> MappedResponse[TResult]:
29
+ raise NotImplementedError
30
+
31
+
32
+ def parse_action_error(value: Any) -> LinkedApiActionError | None:
33
+ if value is None:
34
+ return None
35
+ if isinstance(value, LinkedApiActionError):
36
+ return value
37
+ return LinkedApiActionError.model_validate(value)
38
+
39
+
40
+ def collect_action_errors(values: list[Any]) -> list[LinkedApiActionError]:
41
+ errors: list[LinkedApiActionError] = []
42
+ for value in values:
43
+ parsed = parse_action_error(value)
44
+ if parsed is not None:
45
+ errors.append(parsed)
46
+ return errors
47
+
48
+
49
+ def parse_result(data: Any, model: type[BaseModel] | None) -> Any:
50
+ if data is None or model is None:
51
+ return data
52
+ try:
53
+ if isinstance(data, list):
54
+ return [model.model_validate(item) for item in data]
55
+ return model.model_validate(data)
56
+ except ValidationError as error:
57
+ raise LinkedApiError(
58
+ "unknownError",
59
+ f"Failed to parse API response: {error}",
60
+ details=error.errors(),
61
+ ) from error
62
+
63
+
64
+ def as_action_dict(action: Any) -> dict[str, Any]:
65
+ if isinstance(action, BaseModel):
66
+ return action.model_dump(by_alias=True, exclude_none=True)
67
+ if isinstance(action, dict):
68
+ return action
69
+ msg = "Expected action object"
70
+ raise TypeError(msg)
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Generic, TypeVar, cast
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from linkedapi.mappers.base import (
8
+ BaseMapper,
9
+ MappedResponse,
10
+ as_action_dict,
11
+ collect_action_errors,
12
+ parse_action_error,
13
+ parse_result,
14
+ )
15
+ from linkedapi.types import WorkflowCompletion, WorkflowDefinition, serialize_model
16
+
17
+ TParams = TypeVar("TParams")
18
+ TResult = TypeVar("TResult")
19
+
20
+
21
+ class SimpleWorkflowMapper(BaseMapper[TParams, TResult], Generic[TParams, TResult]):
22
+ def __init__(
23
+ self,
24
+ action_type: str,
25
+ default_params: dict[str, Any] | None = None,
26
+ result_model: type[BaseModel] | None = None,
27
+ ) -> None:
28
+ self.action_type = action_type
29
+ self.default_params = default_params or {}
30
+ self.result_model = result_model
31
+
32
+ def map_request(self, params: TParams | None = None) -> WorkflowDefinition:
33
+ return {"actionType": self.action_type, **self.default_params, **serialize_model(params)}
34
+
35
+ def map_response(self, completion: WorkflowCompletion) -> MappedResponse[TResult]:
36
+ if isinstance(completion, list):
37
+ data = [
38
+ as_action_dict(action).get("data")
39
+ for action in completion
40
+ if as_action_dict(action).get("data")
41
+ ]
42
+ errors = collect_action_errors(
43
+ [as_action_dict(action).get("error") for action in completion]
44
+ )
45
+ return MappedResponse(
46
+ data=cast(TResult, parse_result(data, self.result_model)), errors=errors
47
+ )
48
+
49
+ action = as_action_dict(completion)
50
+ error = parse_action_error(action.get("error"))
51
+ if error is not None:
52
+ return MappedResponse(data=None, errors=[error])
53
+ return MappedResponse(
54
+ data=cast(TResult, parse_result(action.get("data"), self.result_model)),
55
+ errors=[],
56
+ )