pinexq-client 0.3.0.20240620.2__py3-none-any.whl → 0.4.2.20241009.1__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.
- pinexq_client/core/enterapi.py +8 -9
- pinexq_client/core/exceptions.py +73 -2
- pinexq_client/core/hco/action_hco.py +12 -22
- pinexq_client/core/hco/action_with_parameters_hco.py +15 -21
- pinexq_client/core/hco/download_link_hco.py +2 -3
- pinexq_client/core/hco/hco_base.py +2 -3
- pinexq_client/core/hco/link_hco.py +10 -9
- pinexq_client/core/hco/unavailable.py +17 -0
- pinexq_client/core/hco/upload_action_hco.py +11 -10
- pinexq_client/core/model/error.py +5 -1
- pinexq_client/core/sirenaccess.py +18 -27
- pinexq_client/job_management/__init__.py +1 -1
- pinexq_client/job_management/enterjma.py +2 -4
- pinexq_client/job_management/hcos/entrypoint_hco.py +36 -35
- pinexq_client/job_management/hcos/input_dataslot_hco.py +53 -51
- pinexq_client/job_management/hcos/job_hco.py +121 -120
- pinexq_client/job_management/hcos/job_query_result_hco.py +72 -47
- pinexq_client/job_management/hcos/jobsroot_hco.py +44 -42
- pinexq_client/job_management/hcos/output_dataslot_hco.py +1 -1
- pinexq_client/job_management/hcos/processing_step_hco.py +71 -70
- pinexq_client/job_management/hcos/processingstep_query_result_hco.py +76 -51
- pinexq_client/job_management/hcos/processingsteproot_hco.py +44 -43
- pinexq_client/job_management/hcos/workdata_hco.py +81 -80
- pinexq_client/job_management/hcos/workdata_query_result_hco.py +75 -52
- pinexq_client/job_management/hcos/workdataroot_hco.py +53 -52
- pinexq_client/job_management/model/open_api_generated.py +3 -1
- pinexq_client/job_management/tool/job.py +108 -11
- pinexq_client/job_management/tool/job_group.py +158 -0
- pinexq_client/job_management/tool/processing_step.py +83 -5
- pinexq_client/job_management/tool/workdata.py +8 -0
- {pinexq_client-0.3.0.20240620.2.dist-info → pinexq_client-0.4.2.20241009.1.dist-info}/METADATA +2 -2
- pinexq_client-0.4.2.20241009.1.dist-info/RECORD +53 -0
- {pinexq_client-0.3.0.20240620.2.dist-info → pinexq_client-0.4.2.20241009.1.dist-info}/WHEEL +1 -1
- pinexq_client-0.4.2.20241009.1.dist-info/entry_points.txt +4 -0
- pinexq_client-0.3.0.20240620.2.dist-info/RECORD +0 -50
- {pinexq_client-0.3.0.20240620.2.dist-info → pinexq_client-0.4.2.20241009.1.dist-info}/licenses/LICENSE +0 -0
pinexq_client/core/enterapi.py
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
from typing import TypeVar, Type
|
|
2
|
-
|
|
3
1
|
import httpx
|
|
4
2
|
|
|
5
|
-
from
|
|
3
|
+
from typing import TypeVar, Type
|
|
4
|
+
from pinexq_client.core import Entity, get_resource, raise_exception_on_error
|
|
6
5
|
from pinexq_client.core.hco.hco_base import Hco
|
|
7
6
|
|
|
8
7
|
THco = TypeVar('THco', bound=Hco)
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
def enter_api(client: httpx.Client,
|
|
10
|
+
def enter_api(client: httpx.Client,
|
|
11
|
+
entrypoint_hco_type: Type[THco],
|
|
12
|
+
entrypoint_entity_type: Type[Entity] = Entity,
|
|
12
13
|
entrypoint: str = "api/EntryPoint") -> THco:
|
|
13
|
-
entry_point_response = client
|
|
14
|
-
entry_point_response
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return entrypoint_hco_type.from_entity(entrypoint_entity, client)
|
|
14
|
+
entry_point_response = get_resource(client=client, href=entrypoint, parse_type=entrypoint_entity_type)
|
|
15
|
+
raise_exception_on_error("Error accessing the API", entry_point_response)
|
|
16
|
+
return entrypoint_hco_type.from_entity(entry_point_response, client)
|
pinexq_client/core/exceptions.py
CHANGED
|
@@ -1,2 +1,73 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
from httpx import Response, URL
|
|
2
|
+
|
|
3
|
+
from pinexq_client.core.model.error import ProblemDetails
|
|
4
|
+
from .model.sirenmodels import TEntity
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ClientException(Exception):
|
|
8
|
+
"""
|
|
9
|
+
Base class for all exceptions that are thrown by the PinexQ client.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, message: str):
|
|
13
|
+
self.message = message
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NotAvailableException(ClientException):
|
|
17
|
+
"""
|
|
18
|
+
Exception that is thrown when an action or a link is not available.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ApiException(Exception):
|
|
23
|
+
"""
|
|
24
|
+
Base class for all exceptions that are thrown by the PinexQ API.
|
|
25
|
+
"""
|
|
26
|
+
status: int = None # default status code, can be overridden in subclasses
|
|
27
|
+
problem_details: ProblemDetails | None = None
|
|
28
|
+
|
|
29
|
+
def __init__(self, message: str, problem_details: ProblemDetails | None = None):
|
|
30
|
+
super().__init__(message)
|
|
31
|
+
self.problem_details = problem_details
|
|
32
|
+
|
|
33
|
+
def __str__(self) -> str:
|
|
34
|
+
message = super().__str__()
|
|
35
|
+
if self.problem_details:
|
|
36
|
+
message += f"\n{self.problem_details}"
|
|
37
|
+
return message
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TemporarilyNotAvailableException(ApiException):
|
|
41
|
+
"""
|
|
42
|
+
Exception that is thrown when the API returns a 503 status code.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
status: int = 503
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class TooManyRequestsException(ApiException):
|
|
49
|
+
"""
|
|
50
|
+
Exception that is thrown when the API returns a 429 status code.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
status: int = 429
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def raise_exception_on_error(message: str, response: TEntity | Response | ProblemDetails | URL | None):
|
|
57
|
+
match response:
|
|
58
|
+
case ProblemDetails() as problem_details:
|
|
59
|
+
match problem_details.status:
|
|
60
|
+
case 429:
|
|
61
|
+
raise TooManyRequestsException(message, problem_details)
|
|
62
|
+
case 503:
|
|
63
|
+
raise TemporarilyNotAvailableException(message, problem_details)
|
|
64
|
+
case _:
|
|
65
|
+
raise ApiException(message, problem_details)
|
|
66
|
+
case Response() as http_response:
|
|
67
|
+
match http_response.status_code:
|
|
68
|
+
case 429:
|
|
69
|
+
raise TooManyRequestsException(message)
|
|
70
|
+
case 503:
|
|
71
|
+
raise TemporarilyNotAvailableException(message)
|
|
72
|
+
case _:
|
|
73
|
+
raise ApiException(message)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from typing import TypeVar, Self
|
|
2
2
|
|
|
3
3
|
import httpx
|
|
4
|
-
from httpx import Response
|
|
5
4
|
from httpx import URL
|
|
6
5
|
|
|
7
|
-
from pinexq_client.core import
|
|
6
|
+
from pinexq_client.core import Entity, Action, execute_action, raise_exception_on_error, ClientException
|
|
8
7
|
from pinexq_client.core.hco.hco_base import ClientContainer
|
|
8
|
+
from pinexq_client.core.hco.unavailable import UnavailableAction
|
|
9
9
|
|
|
10
10
|
TEntity = TypeVar('TEntity', bound=Entity)
|
|
11
11
|
THcoEntity = TypeVar('THcoEntity', bound=Entity)
|
|
@@ -16,21 +16,21 @@ class ActionHco(ClientContainer):
|
|
|
16
16
|
_action: Action
|
|
17
17
|
|
|
18
18
|
@classmethod
|
|
19
|
-
def from_action_optional(cls, client: httpx.Client, action: Action | None) -> Self |
|
|
19
|
+
def from_action_optional(cls, client: httpx.Client, action: Action | None) -> Self | UnavailableAction:
|
|
20
20
|
if action is None:
|
|
21
|
-
return
|
|
21
|
+
return UnavailableAction()
|
|
22
22
|
|
|
23
23
|
if action.has_parameters():
|
|
24
|
-
raise
|
|
24
|
+
raise ClientException(f"Error while mapping action: expected action no parameters but got some")
|
|
25
25
|
|
|
26
26
|
instance = cls(client)
|
|
27
27
|
instance._action = action
|
|
28
28
|
return instance
|
|
29
29
|
|
|
30
30
|
@classmethod
|
|
31
|
-
def from_entity_optional(cls, client: httpx.Client, entity: Entity, name: str) -> Self |
|
|
31
|
+
def from_entity_optional(cls, client: httpx.Client, entity: Entity, name: str) -> Self | UnavailableAction:
|
|
32
32
|
if entity is None:
|
|
33
|
-
return
|
|
33
|
+
return UnavailableAction()
|
|
34
34
|
|
|
35
35
|
action = entity.find_first_action_with_name(name)
|
|
36
36
|
return cls.from_action_optional(client, action)
|
|
@@ -38,33 +38,23 @@ class ActionHco(ClientContainer):
|
|
|
38
38
|
@classmethod
|
|
39
39
|
def from_action(cls, client: httpx.Client, action: Action) -> Self:
|
|
40
40
|
action = cls.from_action_optional(client, action)
|
|
41
|
-
if action
|
|
42
|
-
raise
|
|
41
|
+
if isinstance(action, UnavailableAction):
|
|
42
|
+
raise ClientException(
|
|
43
43
|
f"Error while mapping mandatory action: does not exist")
|
|
44
44
|
return action
|
|
45
45
|
|
|
46
46
|
@classmethod
|
|
47
47
|
def from_entity(cls, client: httpx.Client, entity: Entity, name: str) -> Self:
|
|
48
48
|
result = cls.from_entity_optional(client, entity, name)
|
|
49
|
-
if result
|
|
50
|
-
raise
|
|
49
|
+
if isinstance(result, UnavailableAction):
|
|
50
|
+
raise ClientException(
|
|
51
51
|
f"Error while mapping mandatory action {name}: does not exist")
|
|
52
52
|
return result
|
|
53
53
|
|
|
54
54
|
def _execute_internal(self) -> None | URL:
|
|
55
55
|
response = execute_action(self._client, self._action)
|
|
56
|
-
|
|
57
|
-
if isinstance(response, ProblemDetails):
|
|
58
|
-
raise SirenException(
|
|
59
|
-
f"Error while executing action: {response}")
|
|
60
|
-
if isinstance(response, Response):
|
|
61
|
-
raise SirenException(
|
|
62
|
-
f"Error while executing action, unexpected response: {response}")
|
|
56
|
+
raise_exception_on_error(f"Error while executing action, unexpected response", response)
|
|
63
57
|
return response
|
|
64
58
|
|
|
65
59
|
def __repr__(self):
|
|
66
60
|
return f"<{self.__class__.__name__}: '{self._action.name}'>"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from typing import TypeVar, Type, Self, Generic
|
|
2
2
|
|
|
3
3
|
import httpx
|
|
4
|
-
from httpx import Response
|
|
5
4
|
from httpx import URL
|
|
6
5
|
from pydantic import BaseModel
|
|
7
6
|
|
|
8
|
-
from pinexq_client.core import
|
|
7
|
+
from pinexq_client.core import Entity, Action, execute_action, raise_exception_on_error, ClientException
|
|
9
8
|
from pinexq_client.core.hco.hco_base import ClientContainer
|
|
9
|
+
from pinexq_client.core.hco.unavailable import UnavailableAction
|
|
10
10
|
|
|
11
11
|
TParameters = TypeVar('TParameters', bound=BaseModel)
|
|
12
12
|
|
|
@@ -16,12 +16,12 @@ class ActionWithParametersHco(ClientContainer, Generic[TParameters]):
|
|
|
16
16
|
_action: Action
|
|
17
17
|
|
|
18
18
|
@classmethod
|
|
19
|
-
def from_action_optional(cls, client: httpx.Client, action: Action | None) -> Self |
|
|
19
|
+
def from_action_optional(cls, client: httpx.Client, action: Action | None) -> Self | UnavailableAction:
|
|
20
20
|
if action is None:
|
|
21
|
-
return
|
|
21
|
+
return UnavailableAction()
|
|
22
22
|
|
|
23
23
|
if not action.has_parameters():
|
|
24
|
-
raise
|
|
24
|
+
raise ClientException(
|
|
25
25
|
f"Error while mapping action: expected action with parameters but got none")
|
|
26
26
|
|
|
27
27
|
instance = cls(client)
|
|
@@ -29,9 +29,9 @@ class ActionWithParametersHco(ClientContainer, Generic[TParameters]):
|
|
|
29
29
|
return instance
|
|
30
30
|
|
|
31
31
|
@classmethod
|
|
32
|
-
def from_entity_optional(cls, client: httpx.Client, entity: Entity, name: str) -> Self |
|
|
32
|
+
def from_entity_optional(cls, client: httpx.Client, entity: Entity, name: str) -> Self | UnavailableAction:
|
|
33
33
|
if entity is None:
|
|
34
|
-
return
|
|
34
|
+
return UnavailableAction()
|
|
35
35
|
|
|
36
36
|
action = entity.find_first_action_with_name(name)
|
|
37
37
|
return cls.from_action_optional(client, action)
|
|
@@ -39,43 +39,37 @@ class ActionWithParametersHco(ClientContainer, Generic[TParameters]):
|
|
|
39
39
|
@classmethod
|
|
40
40
|
def from_action(cls, client: httpx.Client, action: Action) -> Self:
|
|
41
41
|
action = cls.from_action_optional(client, action)
|
|
42
|
-
if action
|
|
43
|
-
raise
|
|
42
|
+
if isinstance(action, UnavailableAction):
|
|
43
|
+
raise ClientException(
|
|
44
44
|
f"Error while mapping mandatory action: action does not exist")
|
|
45
45
|
return action
|
|
46
46
|
|
|
47
47
|
@classmethod
|
|
48
48
|
def from_entity(cls, client: httpx.Client, entity: Entity, name: str) -> Self:
|
|
49
49
|
result = cls.from_entity_optional(client, entity, name)
|
|
50
|
-
if result
|
|
51
|
-
raise
|
|
50
|
+
if isinstance(result, UnavailableAction):
|
|
51
|
+
raise ClientException(
|
|
52
52
|
f"Error while mapping mandatory action {name}: action does not exist")
|
|
53
53
|
return result
|
|
54
54
|
|
|
55
55
|
def _execute_internal(self, parameters: BaseModel) -> None | URL:
|
|
56
56
|
if parameters is None:
|
|
57
|
-
raise
|
|
57
|
+
raise ClientException(f"Error while executing action: action requires parameters")
|
|
58
58
|
|
|
59
59
|
response = execute_action(self._client, self._action, parameters)
|
|
60
|
-
|
|
61
|
-
if isinstance(response, ProblemDetails):
|
|
62
|
-
raise SirenException(
|
|
63
|
-
f"Error while executing action: {response}")
|
|
64
|
-
if isinstance(response, Response):
|
|
65
|
-
raise SirenException(
|
|
66
|
-
f"Error while executing action, unexpected response: {response}")
|
|
60
|
+
raise_exception_on_error(f"Error while executing action, unexpected response", response)
|
|
67
61
|
return response
|
|
68
62
|
|
|
69
63
|
def _execute(self, parameters: TParameters):
|
|
70
64
|
result = self._execute_internal(parameters)
|
|
71
65
|
if result is not None:
|
|
72
|
-
raise
|
|
66
|
+
raise ClientException("Action did respond with unexpected URL")
|
|
73
67
|
return
|
|
74
68
|
|
|
75
69
|
def _execute_returns_url(self, parameters: TParameters) -> URL:
|
|
76
70
|
result = self._execute_internal(parameters)
|
|
77
71
|
if result is None:
|
|
78
|
-
raise
|
|
72
|
+
raise ClientException("Action did not respond with URL")
|
|
79
73
|
return result
|
|
80
74
|
|
|
81
75
|
def _get_default_parameters(self, parameter_type: Type[TParameters],
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
from io import BytesIO
|
|
2
1
|
from typing import Self
|
|
3
2
|
|
|
4
3
|
import httpx
|
|
5
4
|
from httpx import Response
|
|
6
5
|
|
|
7
|
-
from pinexq_client.core import Link, Entity, get_resource,
|
|
6
|
+
from pinexq_client.core import Link, Entity, get_resource, ClientException
|
|
8
7
|
from pinexq_client.core.hco.link_hco import LinkHco
|
|
9
8
|
|
|
10
9
|
|
|
@@ -29,7 +28,7 @@ class DownloadLinkHco(LinkHco):
|
|
|
29
28
|
def download(self) -> bytes:
|
|
30
29
|
response: Response = get_resource(self._client, self._link.href, self._link.type)
|
|
31
30
|
if not isinstance(response, Response):
|
|
32
|
-
raise
|
|
31
|
+
raise ClientException(
|
|
33
32
|
f"Error while downloading resource: did not get response type")
|
|
34
33
|
|
|
35
34
|
return response.content
|
|
@@ -4,7 +4,7 @@ from typing import List, TypeVar, Generic, Callable, Any
|
|
|
4
4
|
import httpx
|
|
5
5
|
from httpx import URL
|
|
6
6
|
|
|
7
|
-
from pinexq_client.core import
|
|
7
|
+
from pinexq_client.core import Entity, BaseRelations, Link, ClientException
|
|
8
8
|
|
|
9
9
|
TEntity = TypeVar('TEntity', bound=Entity)
|
|
10
10
|
THcoEntity = TypeVar('THcoEntity', bound=Entity)
|
|
@@ -64,7 +64,7 @@ class Hco(ClientContainer, Generic[THcoEntity]):
|
|
|
64
64
|
def check_classes(existing_classes: List[str], expected_classes: List[str]):
|
|
65
65
|
for expected_class in expected_classes:
|
|
66
66
|
if expected_class not in existing_classes:
|
|
67
|
-
raise
|
|
67
|
+
raise ClientException(
|
|
68
68
|
f"Error while mapping entity:expected hco class {expected_class} is not a class of generic entity "
|
|
69
69
|
f"with classes {existing_classes}")
|
|
70
70
|
|
|
@@ -88,4 +88,3 @@ class Hco(ClientContainer, Generic[THcoEntity]):
|
|
|
88
88
|
if hco_property.converter:
|
|
89
89
|
property_value = hco_property.converter(property_value)
|
|
90
90
|
setattr(self, var_name, property_value)
|
|
91
|
-
|
|
@@ -3,8 +3,9 @@ from typing import Self, Type
|
|
|
3
3
|
import httpx
|
|
4
4
|
from httpx import URL
|
|
5
5
|
|
|
6
|
-
from pinexq_client.core import Link,
|
|
6
|
+
from pinexq_client.core import Link, Entity, navigate, ensure_siren_response, ClientException
|
|
7
7
|
from pinexq_client.core.hco.hco_base import ClientContainer, TEntity
|
|
8
|
+
from pinexq_client.core.hco.unavailable import UnavailableLink
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class LinkHco(ClientContainer):
|
|
@@ -12,18 +13,18 @@ class LinkHco(ClientContainer):
|
|
|
12
13
|
_link: Link
|
|
13
14
|
|
|
14
15
|
@classmethod
|
|
15
|
-
def from_link_optional(cls, client: httpx.Client, link: Link | None) -> Self |
|
|
16
|
+
def from_link_optional(cls, client: httpx.Client, link: Link | None) -> Self | UnavailableLink:
|
|
16
17
|
if link is None:
|
|
17
|
-
return
|
|
18
|
+
return UnavailableLink()
|
|
18
19
|
|
|
19
20
|
instance = cls(client)
|
|
20
21
|
instance._link = link
|
|
21
22
|
return instance
|
|
22
23
|
|
|
23
24
|
@classmethod
|
|
24
|
-
def from_entity_optional(cls, client: httpx.Client, entity: Entity, link_relation: str) -> Self |
|
|
25
|
+
def from_entity_optional(cls, client: httpx.Client, entity: Entity, link_relation: str) -> Self | UnavailableLink:
|
|
25
26
|
if entity is None:
|
|
26
|
-
return
|
|
27
|
+
return UnavailableLink()
|
|
27
28
|
|
|
28
29
|
link = entity.find_first_link_with_relation(link_relation)
|
|
29
30
|
return cls.from_link_optional(client, link)
|
|
@@ -31,16 +32,16 @@ class LinkHco(ClientContainer):
|
|
|
31
32
|
@classmethod
|
|
32
33
|
def from_link(cls, client: httpx.Client, link: Link) -> Self:
|
|
33
34
|
result = cls.from_link_optional(client, link)
|
|
34
|
-
if result
|
|
35
|
-
raise
|
|
35
|
+
if isinstance(result, UnavailableLink):
|
|
36
|
+
raise ClientException(f"Error while mapping mandatory link: link is None")
|
|
36
37
|
|
|
37
38
|
return result
|
|
38
39
|
|
|
39
40
|
@classmethod
|
|
40
41
|
def from_entity(cls, client: httpx.Client, entity: Entity, link_relation: str) -> Self:
|
|
41
42
|
result = cls.from_entity_optional(client, entity, link_relation)
|
|
42
|
-
if result
|
|
43
|
-
raise
|
|
43
|
+
if isinstance(result, UnavailableLink):
|
|
44
|
+
raise ClientException(
|
|
44
45
|
f"Error while mapping mandatory link: entity contains no link with relation {link_relation}")
|
|
45
46
|
|
|
46
47
|
return result
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pinexq_client.core import NotAvailableException
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class UnavailableAction:
|
|
5
|
+
"""This class is used to represent an action that is not available. It is used to avoid None
|
|
6
|
+
checks in the code."""
|
|
7
|
+
|
|
8
|
+
def execute(self, *args, **kwargs):
|
|
9
|
+
raise NotAvailableException(f"Error while executing action: action is not available")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class UnavailableLink:
|
|
13
|
+
"""This class is used to represent a link that is not available. It is used to avoid None
|
|
14
|
+
checks in the code."""
|
|
15
|
+
|
|
16
|
+
def navigate(self):
|
|
17
|
+
raise NotAvailableException(f"Error while navigating: link is not available")
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
from io import IOBase
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Iterable, AsyncIterable, Any, Self
|
|
3
3
|
|
|
4
4
|
import httpx
|
|
5
5
|
from httpx import URL
|
|
6
6
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
7
7
|
|
|
8
|
-
from pinexq_client.core import upload_file, upload_binary, upload_json,
|
|
9
|
-
SirenClasses
|
|
8
|
+
from pinexq_client.core import upload_file, upload_binary, upload_json, MediaTypes, Action, Entity, \
|
|
9
|
+
SirenClasses, ClientException
|
|
10
10
|
from pinexq_client.core.hco.action_with_parameters_hco import ActionWithParametersHco
|
|
11
|
+
from pinexq_client.core.hco.unavailable import UnavailableAction
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class UploadParameters(BaseModel):
|
|
@@ -42,7 +43,7 @@ class UploadAction(ActionWithParametersHco[UploadParameters]):
|
|
|
42
43
|
def from_action_optional(cls, client: httpx.Client, action: Action | None) -> Self | None:
|
|
43
44
|
instance = super(UploadAction, cls).from_action_optional(client, action)
|
|
44
45
|
|
|
45
|
-
if instance:
|
|
46
|
+
if not isinstance(instance, UnavailableAction):
|
|
46
47
|
cls.validate_file_upload_field(instance)
|
|
47
48
|
cls.assign_upload_constraints(instance)
|
|
48
49
|
return instance
|
|
@@ -51,7 +52,7 @@ class UploadAction(ActionWithParametersHco[UploadParameters]):
|
|
|
51
52
|
def from_entity_optional(cls, client: httpx.Client, entity: Entity, name: str) -> Self | None:
|
|
52
53
|
instance = super(UploadAction, cls).from_entity_optional(client, entity, name)
|
|
53
54
|
|
|
54
|
-
if instance:
|
|
55
|
+
if not isinstance(instance, UnavailableAction):
|
|
55
56
|
cls.validate_file_upload_field(instance)
|
|
56
57
|
cls.assign_upload_constraints(instance)
|
|
57
58
|
return instance
|
|
@@ -80,17 +81,17 @@ class UploadAction(ActionWithParametersHco[UploadParameters]):
|
|
|
80
81
|
def validate_file_upload_field(self):
|
|
81
82
|
action = self._action
|
|
82
83
|
if SirenClasses.FileUploadAction not in action.class_:
|
|
83
|
-
raise
|
|
84
|
+
raise ClientException(
|
|
84
85
|
f"Upload action does not have expected class: {str(SirenClasses.FileUploadAction)}. Got: {action.class_}")
|
|
85
86
|
|
|
86
87
|
if action.type != MediaTypes.MULTIPART_FORM_DATA.value:
|
|
87
|
-
raise
|
|
88
|
+
raise ClientException(
|
|
88
89
|
f"Upload action does not have expected type: {str(MediaTypes.MULTIPART_FORM_DATA)}. Got: {action.type}")
|
|
89
90
|
|
|
90
91
|
upload_fields = self._action.fields[0]
|
|
91
92
|
|
|
92
93
|
if upload_fields.type != "file":
|
|
93
|
-
raise
|
|
94
|
+
raise ClientException(
|
|
94
95
|
f"Upload action does not have expected field type: 'file'. Got: {upload_fields.type}")
|
|
95
96
|
|
|
96
97
|
def _upload(self, parameters: UploadParameters) -> URL:
|
|
@@ -106,8 +107,8 @@ class UploadAction(ActionWithParametersHco[UploadParameters]):
|
|
|
106
107
|
elif parameters.json_ is not None:
|
|
107
108
|
result = upload_json(self._client, self._action, parameters.json_, parameters.filename)
|
|
108
109
|
else:
|
|
109
|
-
raise
|
|
110
|
+
raise ClientException("Did not execute upload, none selected")
|
|
110
111
|
|
|
111
112
|
if not isinstance(result, URL):
|
|
112
|
-
raise
|
|
113
|
+
raise ClientException("Upload did not respond with location")
|
|
113
114
|
return result
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from pydantic import BaseModel
|
|
1
|
+
from pydantic import BaseModel, Field, ConfigDict
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
class ProblemDetails(BaseModel):
|
|
@@ -7,3 +7,7 @@ class ProblemDetails(BaseModel):
|
|
|
7
7
|
status: int | None = None
|
|
8
8
|
detail: str | None = None
|
|
9
9
|
instance: str | None = None
|
|
10
|
+
|
|
11
|
+
def __str__(self):
|
|
12
|
+
message = [f" {key}: {value}" for key, value in self.model_dump().items() if value]
|
|
13
|
+
return str.join("\n", message)
|
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import
|
|
2
|
+
import warnings
|
|
3
3
|
from io import BytesIO
|
|
4
4
|
from typing import Any, BinaryIO, Iterable, AsyncIterable
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
7
|
-
from httpx import Response
|
|
8
|
-
from httpx import URL
|
|
7
|
+
from httpx import Response, URL
|
|
9
8
|
from pydantic import BaseModel
|
|
10
9
|
|
|
11
|
-
from .exceptions import SirenException
|
|
12
|
-
from .http_headers import Headers
|
|
13
10
|
from .base_relations import BaseRelations
|
|
11
|
+
from .exceptions import raise_exception_on_error, ClientException
|
|
12
|
+
from .http_headers import Headers
|
|
14
13
|
from .media_types import MediaTypes
|
|
15
14
|
from .model.error import ProblemDetails
|
|
16
15
|
from .model.sirenmodels import Entity, Link, Action, TEntity
|
|
17
16
|
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
17
|
|
|
21
18
|
# for now, we do not support navigations to non siren links (e.g. external)
|
|
22
19
|
def get_resource(client: httpx.Client, href: str, media_type: str = MediaTypes.SIREN,
|
|
@@ -25,13 +22,13 @@ def get_resource(client: httpx.Client, href: str, media_type: str = MediaTypes.S
|
|
|
25
22
|
# assume get for links
|
|
26
23
|
response = client.get(href)
|
|
27
24
|
except (httpx.ConnectTimeout, httpx.ConnectError) as exc:
|
|
28
|
-
raise
|
|
25
|
+
raise ClientException(f"Http-client error requesting resource: {href}") from exc
|
|
29
26
|
expected_type = media_type or MediaTypes.SIREN # if not specified expect siren
|
|
30
27
|
|
|
31
28
|
if response.status_code == httpx.codes.OK:
|
|
32
29
|
if (media_type := response.headers.get(Headers.CONTENT_TYPE, MediaTypes.SIREN)) != expected_type:
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
warnings.warn(f"Expected type {expected_type} not matched by response: "
|
|
31
|
+
f"' got: '{media_type}'")
|
|
35
32
|
|
|
36
33
|
if media_type == MediaTypes.SIREN.value or media_type is None: # assume siren if not specified
|
|
37
34
|
resp = response.content.decode()
|
|
@@ -43,7 +40,7 @@ def get_resource(client: httpx.Client, href: str, media_type: str = MediaTypes.S
|
|
|
43
40
|
elif response.status_code >= 400:
|
|
44
41
|
return handle_error_response(response)
|
|
45
42
|
else:
|
|
46
|
-
|
|
43
|
+
warnings.warn(f"Unexpected return code: {response.status_code}")
|
|
47
44
|
return response
|
|
48
45
|
|
|
49
46
|
|
|
@@ -53,12 +50,7 @@ def navigate(client: httpx.Client, link: Link,
|
|
|
53
50
|
|
|
54
51
|
|
|
55
52
|
def ensure_siren_response(response: TEntity | ProblemDetails | Response) -> TEntity:
|
|
56
|
-
|
|
57
|
-
raise SirenException(
|
|
58
|
-
f"Error while navigating: {response}")
|
|
59
|
-
if isinstance(response, Response):
|
|
60
|
-
raise SirenException(
|
|
61
|
-
f"Error while navigating, unexpected response: {response}")
|
|
53
|
+
raise_exception_on_error(f"Error while navigating, unexpected response", response)
|
|
62
54
|
return response
|
|
63
55
|
|
|
64
56
|
|
|
@@ -70,7 +62,6 @@ def upload_json(client: httpx.Client, action: Action, json_payload: Any,
|
|
|
70
62
|
def upload_binary(client: httpx.Client, action: Action, content: str | bytes | Iterable[bytes] | AsyncIterable[bytes],
|
|
71
63
|
filename: str,
|
|
72
64
|
mediatype: str = MediaTypes.OCTET_STREAM) -> None | URL | ProblemDetails | Response:
|
|
73
|
-
|
|
74
65
|
if isinstance(content, str):
|
|
75
66
|
payload = BytesIO(content.encode(encoding="utf-8"))
|
|
76
67
|
elif isinstance(content, bytes):
|
|
@@ -86,21 +77,21 @@ def upload_binary(client: httpx.Client, action: Action, content: str | bytes | I
|
|
|
86
77
|
def upload_file(client: httpx.Client, action: Action, file: BinaryIO, filename: str,
|
|
87
78
|
mediatype: str = MediaTypes.OCTET_STREAM) -> None | URL | ProblemDetails | Response:
|
|
88
79
|
if action.type != MediaTypes.MULTIPART_FORM_DATA:
|
|
89
|
-
raise
|
|
80
|
+
raise ClientException(
|
|
90
81
|
f"Action with upload requires type: {MediaTypes.MULTIPART_FORM_DATA} but found: {action.type}")
|
|
91
82
|
|
|
92
83
|
files = {'upload-file': (filename, file, mediatype)}
|
|
93
84
|
try:
|
|
94
85
|
response = client.request(method=action.method, url=action.href, files=files)
|
|
95
86
|
except httpx.RequestError as exc:
|
|
96
|
-
raise
|
|
87
|
+
raise ClientException(f"Error from httpx while uploading data to: {action.href}") from exc
|
|
97
88
|
return handle_action_result(response)
|
|
98
89
|
|
|
99
90
|
|
|
100
91
|
def execute_action_on_entity(client: httpx.Client, entity: Entity, name: str, parameters: BaseModel | None = None):
|
|
101
92
|
action = entity.find_first_action_with_name(name)
|
|
102
93
|
if action is None:
|
|
103
|
-
raise
|
|
94
|
+
raise ClientException(f"Entity does not contain expected action: {name}")
|
|
104
95
|
|
|
105
96
|
return execute_action(client, action, parameters)
|
|
106
97
|
|
|
@@ -110,11 +101,11 @@ def execute_action(client: httpx.Client, action: Action,
|
|
|
110
101
|
if action.has_parameters() is False:
|
|
111
102
|
# no parameters required
|
|
112
103
|
if parameters is not None:
|
|
113
|
-
raise
|
|
104
|
+
raise ClientException(f"Action requires no parameters but got some")
|
|
114
105
|
else:
|
|
115
106
|
# parameters required
|
|
116
107
|
if parameters is None:
|
|
117
|
-
raise
|
|
108
|
+
raise ClientException(f"Action requires parameters but non provided")
|
|
118
109
|
|
|
119
110
|
action_parameters = None
|
|
120
111
|
if parameters is not None:
|
|
@@ -128,7 +119,7 @@ def execute_action(client: httpx.Client, action: Action,
|
|
|
128
119
|
headers={Headers.CONTENT_TYPE.value: MediaTypes.APPLICATION_JSON.value}
|
|
129
120
|
)
|
|
130
121
|
except httpx.RequestError as exc:
|
|
131
|
-
raise
|
|
122
|
+
raise ClientException(f"Error from httpx while executing action: {action.href}") from exc
|
|
132
123
|
|
|
133
124
|
return handle_action_result(response)
|
|
134
125
|
|
|
@@ -139,7 +130,7 @@ def handle_action_result(response: Response) -> None | URL | ProblemDetails | Re
|
|
|
139
130
|
if response.status_code == httpx.codes.CREATED:
|
|
140
131
|
location_header = response.headers.get(Headers.LOCATION_HEADER, None)
|
|
141
132
|
if location_header is None:
|
|
142
|
-
|
|
133
|
+
warnings.warn(f"Got created response without location header")
|
|
143
134
|
return
|
|
144
135
|
|
|
145
136
|
return URL(location_header)
|
|
@@ -147,7 +138,7 @@ def handle_action_result(response: Response) -> None | URL | ProblemDetails | Re
|
|
|
147
138
|
elif response.status_code >= 400:
|
|
148
139
|
return handle_error_response(response)
|
|
149
140
|
else:
|
|
150
|
-
|
|
141
|
+
warnings.warn(f"Unexpected return code: {response.status_code}")
|
|
151
142
|
return response
|
|
152
143
|
|
|
153
144
|
|
|
@@ -156,7 +147,7 @@ def handle_error_response(response: Response) -> ProblemDetails | Response:
|
|
|
156
147
|
if content_type.startswith(MediaTypes.PROBLEM_DETAILS.value):
|
|
157
148
|
return ProblemDetails.model_validate(response.json())
|
|
158
149
|
else:
|
|
159
|
-
|
|
150
|
+
warnings.warn(
|
|
160
151
|
f"Error case did not return media type: '{MediaTypes.PROBLEM_DETAILS.value}', "
|
|
161
152
|
f"got '{response.headers.get(Headers.CONTENT_TYPE, None)}' instead!")
|
|
162
153
|
return response
|
|
@@ -2,7 +2,7 @@ from typing import TypeVar, Type
|
|
|
2
2
|
|
|
3
3
|
import httpx
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import warnings
|
|
6
6
|
|
|
7
7
|
from pinexq_client.core import Entity
|
|
8
8
|
from pinexq_client.core.enterapi import enter_api
|
|
@@ -11,8 +11,6 @@ from pinexq_client.job_management.hcos.entrypoint_hco import EntryPointHco
|
|
|
11
11
|
from pinexq_client.job_management.model.sirenentities import EntryPointEntity
|
|
12
12
|
import pinexq_client.job_management
|
|
13
13
|
|
|
14
|
-
LOG = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
14
|
THco = TypeVar("THco", bound=Hco)
|
|
17
15
|
|
|
18
16
|
|
|
@@ -34,7 +32,7 @@ def enter_jma(
|
|
|
34
32
|
client_version = pinexq_client.job_management.__jma_version__
|
|
35
33
|
jma_version = [int(i) for i in str.split(info.api_version, '.')]
|
|
36
34
|
if not _version_match_major_minor(jma_version, client_version):
|
|
37
|
-
|
|
35
|
+
warnings.warn(
|
|
38
36
|
f"Version mismatch between 'pinexq_client' (v{'.'.join(map(str ,client_version))}) "
|
|
39
37
|
f"and 'JobManagementAPI' (v{'.'.join(map(str, jma_version))})! "
|
|
40
38
|
)
|