pinexq-client 0.2.0.2024.607.8__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.
- hypermedia_client/core/__init__.py +8 -0
- hypermedia_client/core/base_relations.py +8 -0
- hypermedia_client/core/enterapi.py +17 -0
- hypermedia_client/core/exceptions.py +2 -0
- hypermedia_client/core/hco/__init__.py +0 -0
- hypermedia_client/core/hco/action_hco.py +70 -0
- hypermedia_client/core/hco/action_with_parameters_hco.py +86 -0
- hypermedia_client/core/hco/download_link_hco.py +37 -0
- hypermedia_client/core/hco/hco_base.py +91 -0
- hypermedia_client/core/hco/link_hco.py +57 -0
- hypermedia_client/core/hco/upload_action_hco.py +113 -0
- hypermedia_client/core/http_headers.py +9 -0
- hypermedia_client/core/media_types.py +24 -0
- hypermedia_client/core/model/__init__.py +0 -0
- hypermedia_client/core/model/error.py +9 -0
- hypermedia_client/core/model/sirenmodels.py +155 -0
- hypermedia_client/core/polling.py +37 -0
- hypermedia_client/core/sirenaccess.py +173 -0
- hypermedia_client/job_management/__init__.py +6 -0
- hypermedia_client/job_management/enterjma.py +42 -0
- hypermedia_client/job_management/hcos/__init__.py +12 -0
- hypermedia_client/job_management/hcos/entrypoint_hco.py +57 -0
- hypermedia_client/job_management/hcos/info_hco.py +42 -0
- hypermedia_client/job_management/hcos/input_dataslot_hco.py +82 -0
- hypermedia_client/job_management/hcos/job_hco.py +174 -0
- hypermedia_client/job_management/hcos/job_query_result_hco.py +63 -0
- hypermedia_client/job_management/hcos/job_used_tags_hco.py +30 -0
- hypermedia_client/job_management/hcos/jobsroot_hco.py +80 -0
- hypermedia_client/job_management/hcos/output_dataslot_hco.py +44 -0
- hypermedia_client/job_management/hcos/processing_step_hco.py +71 -0
- hypermedia_client/job_management/hcos/processing_step_used_tags_hco.py +30 -0
- hypermedia_client/job_management/hcos/processingstep_query_result_hco.py +68 -0
- hypermedia_client/job_management/hcos/processingsteproot_hco.py +72 -0
- hypermedia_client/job_management/hcos/user_hco.py +37 -0
- hypermedia_client/job_management/hcos/workdata_hco.py +127 -0
- hypermedia_client/job_management/hcos/workdata_query_result_hco.py +67 -0
- hypermedia_client/job_management/hcos/workdata_used_tags_query_result_hco.py +30 -0
- hypermedia_client/job_management/hcos/workdataroot_hco.py +84 -0
- hypermedia_client/job_management/ideas.md +28 -0
- hypermedia_client/job_management/known_relations.py +29 -0
- hypermedia_client/job_management/model/__init__.py +1 -0
- hypermedia_client/job_management/model/open_api_generated.py +890 -0
- hypermedia_client/job_management/model/sirenentities.py +112 -0
- hypermedia_client/job_management/tool/__init__.py +1 -0
- hypermedia_client/job_management/tool/job.py +442 -0
- pinexq_client-0.2.0.2024.607.8.dist-info/METADATA +105 -0
- pinexq_client-0.2.0.2024.607.8.dist-info/RECORD +49 -0
- pinexq_client-0.2.0.2024.607.8.dist-info/WHEEL +4 -0
- pinexq_client-0.2.0.2024.607.8.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import List, Any, Union, Type, TypeVar, Self
|
|
3
|
+
|
|
4
|
+
from httpx import URL
|
|
5
|
+
from pydantic import BaseModel, ConfigDict, constr, Field, field_validator, model_validator
|
|
6
|
+
|
|
7
|
+
TParameters = TypeVar('TParameters', bound='BaseModel')
|
|
8
|
+
TEntity = TypeVar('TEntity', bound='Entity')
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SirenBaseModel(BaseModel):
|
|
12
|
+
model_config = ConfigDict(
|
|
13
|
+
extra='forbid'
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ActionField(SirenBaseModel):
|
|
18
|
+
name: constr(min_length=1)
|
|
19
|
+
type: str | None = None
|
|
20
|
+
value: Any | None = None
|
|
21
|
+
class_: List[str] | None = Field(None, alias='class')
|
|
22
|
+
title: str | None = None
|
|
23
|
+
accept: str | None = None
|
|
24
|
+
maxFileSizeBytes: int | None = None
|
|
25
|
+
allowMultiple: bool | None = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Action(SirenBaseModel):
|
|
29
|
+
name: constr(min_length=1)
|
|
30
|
+
href: constr(min_length=1)
|
|
31
|
+
class_: List[str] | None = Field(None, alias='class')
|
|
32
|
+
method: str | None = None
|
|
33
|
+
title: str | None = None
|
|
34
|
+
type: str | None = None
|
|
35
|
+
fields: List[ActionField] | None = None
|
|
36
|
+
|
|
37
|
+
def has_parameters(self) -> bool:
|
|
38
|
+
return (self.fields is not None) and (len(self.fields) > 0)
|
|
39
|
+
|
|
40
|
+
def get_default_parameters(self, parameter_type: Type[TParameters] = Any,
|
|
41
|
+
default_if_none: TParameters | None = None) -> TParameters | None:
|
|
42
|
+
if not self.has_parameters():
|
|
43
|
+
raise Exception("Can not get default parameters for action without parameters")
|
|
44
|
+
if len(self.fields) > 1:
|
|
45
|
+
raise Exception("Action has more than one field, can not determine default parameters")
|
|
46
|
+
if not self.fields[0].value:
|
|
47
|
+
return default_if_none
|
|
48
|
+
dump = self.fields[0].model_dump(by_alias=True)['value']
|
|
49
|
+
return parameter_type.model_validate(dump)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class EmbeddedLinkEntity(SirenBaseModel):
|
|
53
|
+
class_: List[str] | None = Field(None, alias='class')
|
|
54
|
+
rel: List[str]
|
|
55
|
+
href: constr(min_length=1)
|
|
56
|
+
title: str | None = None
|
|
57
|
+
type: str | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Link(SirenBaseModel):
|
|
61
|
+
href: constr(min_length=1)
|
|
62
|
+
rel: List[str]
|
|
63
|
+
class_: List[str] | None = Field(None, alias='class')
|
|
64
|
+
title: str | None = None
|
|
65
|
+
type: str | None = None
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def from_url(cls,
|
|
69
|
+
url: URL,
|
|
70
|
+
relation: list[str],
|
|
71
|
+
title: str | None = None,
|
|
72
|
+
mediatype: str | None = None,
|
|
73
|
+
class_: list[str] | None = None) -> Self:
|
|
74
|
+
instance = cls(href=str(url), rel=relation, title=title, type=mediatype)
|
|
75
|
+
instance.class_ = class_
|
|
76
|
+
return instance
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Entity(SirenBaseModel):
|
|
80
|
+
rel: List[str] = [] # used when embedded
|
|
81
|
+
class_: List[str] | None = Field(None, alias='class')
|
|
82
|
+
title: str | None = None
|
|
83
|
+
properties: Any | None = None
|
|
84
|
+
entities: List[Union['Entity', EmbeddedLinkEntity]] | None = None
|
|
85
|
+
actions: List[Action] | None = None
|
|
86
|
+
links: List[Link] | None = None
|
|
87
|
+
|
|
88
|
+
@model_validator(mode='after')
|
|
89
|
+
def _check_extra_properties(self) -> Self:
|
|
90
|
+
if type(self) is Entity:
|
|
91
|
+
# skip this validation for the base class
|
|
92
|
+
return self
|
|
93
|
+
if isinstance(self.properties, dict):
|
|
94
|
+
warnings.warn(f"Unresolved properties in Entity '{self.title}'! Implementation might be incomplete?")
|
|
95
|
+
elif isinstance(self.properties, BaseModel) and self.properties.model_extra:
|
|
96
|
+
warnings.warn(
|
|
97
|
+
f"Entity with extra properties received! Possibly a version mismatch "
|
|
98
|
+
f"between server and client? "
|
|
99
|
+
f"(unexpected properties: {[n for n in self.properties.model_extra.keys()]})"
|
|
100
|
+
)
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def find_first_entity_with_relation(self, searched_relation: str, entity_type: Type[TEntity] = 'Entity'
|
|
104
|
+
) -> Union[TEntity, None]:
|
|
105
|
+
if self.entities is None:
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
for entity in self.entities:
|
|
109
|
+
if self.contains_relation(entity.rel, searched_relation):
|
|
110
|
+
return entity_type.model_validate(entity.model_dump(by_alias=True))
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
def find_all_entities_with_relation(self, searched_relation: str,
|
|
114
|
+
entity_type: Type[TEntity] = 'Entity') -> List[TEntity]:
|
|
115
|
+
if self.entities is None:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
result = []
|
|
119
|
+
for entity in self.entities:
|
|
120
|
+
if self.contains_relation(entity.rel, searched_relation):
|
|
121
|
+
# map to requested entity type so properties are not in a dict
|
|
122
|
+
mapped = entity_type.model_validate(entity.model_dump(by_alias=True))
|
|
123
|
+
result.append(mapped)
|
|
124
|
+
return result
|
|
125
|
+
|
|
126
|
+
def find_first_link_with_relation(self, searched_relation: str) -> Link | None:
|
|
127
|
+
if self.links is None:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
for link in self.links:
|
|
131
|
+
if self.contains_relation(link.rel, searched_relation):
|
|
132
|
+
return link
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
def action_exists(self, name: str) -> bool:
|
|
136
|
+
return self.find_first_action_with_name(name) is not None
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def contains_relation(relations: list[str] | None, searched_relation: str) -> bool:
|
|
140
|
+
if relations is None:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
for relation in relations:
|
|
144
|
+
if relation == searched_relation:
|
|
145
|
+
return True
|
|
146
|
+
return False
|
|
147
|
+
|
|
148
|
+
def find_first_action_with_name(self, name: str):
|
|
149
|
+
if self.actions is None:
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
for action in self.actions:
|
|
153
|
+
if action.name == name:
|
|
154
|
+
return action
|
|
155
|
+
return None
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PollingException(Exception):
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def wait_until(
|
|
10
|
+
condition: Callable,
|
|
11
|
+
polling_interval_ms: int = 200,
|
|
12
|
+
timeout_ms: int = 5000,
|
|
13
|
+
timeout_message: str | None = None,
|
|
14
|
+
error_condition: Callable | None = None,
|
|
15
|
+
error_condition_message: str | None = None,
|
|
16
|
+
|
|
17
|
+
) -> None:
|
|
18
|
+
start = time.time()
|
|
19
|
+
timeout = start + timeout_ms / 1000
|
|
20
|
+
while True:
|
|
21
|
+
now = time.time()
|
|
22
|
+
next_due = now + polling_interval_ms / 1000
|
|
23
|
+
success = condition()
|
|
24
|
+
if success:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
if error_condition:
|
|
28
|
+
error = error_condition()
|
|
29
|
+
if error:
|
|
30
|
+
raise PollingException(
|
|
31
|
+
f"{f': {error_condition_message}' if error_condition_message else 'Error condition meet while waiting'}")
|
|
32
|
+
|
|
33
|
+
if (timeout > 0) and (now > timeout):
|
|
34
|
+
raise TimeoutError(
|
|
35
|
+
f"{f': {timeout_message}' if timeout_message else f'Timeout while waiting. Waited: {timeout_ms}ms'}")
|
|
36
|
+
|
|
37
|
+
time.sleep(next_due - now)
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import Any, BinaryIO, Iterable, AsyncIterable
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
from httpx import Response
|
|
8
|
+
from httpx import URL
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from .exceptions import SirenException
|
|
12
|
+
from .http_headers import Headers
|
|
13
|
+
from .base_relations import BaseRelations
|
|
14
|
+
from .media_types import MediaTypes
|
|
15
|
+
from .model.error import ProblemDetails
|
|
16
|
+
from .model.sirenmodels import Entity, Link, Action, TEntity
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# for now, we do not support navigations to non siren links (e.g. external)
|
|
22
|
+
def get_resource(client: httpx.Client, href: str, media_type: str = MediaTypes.SIREN,
|
|
23
|
+
parse_type: type[TEntity] = Entity) -> TEntity | ProblemDetails | Response:
|
|
24
|
+
try:
|
|
25
|
+
# assume get for links
|
|
26
|
+
response = client.get(href)
|
|
27
|
+
except (httpx.ConnectTimeout, httpx.ConnectError) as exc:
|
|
28
|
+
raise SirenException(f"Http-client error requesting resource: {href}") from exc
|
|
29
|
+
expected_type = media_type or MediaTypes.SIREN # if not specified expect siren
|
|
30
|
+
|
|
31
|
+
if response.status_code == httpx.codes.OK:
|
|
32
|
+
if (media_type := response.headers.get(Headers.CONTENT_TYPE, MediaTypes.SIREN)) != expected_type:
|
|
33
|
+
logger.warning(f"Expected type {expected_type} not matched by response: "
|
|
34
|
+
f"' got: '{media_type}'")
|
|
35
|
+
|
|
36
|
+
if media_type == MediaTypes.SIREN.value or media_type is None: # assume siren if not specified
|
|
37
|
+
resp = response.content.decode()
|
|
38
|
+
entity = parse_type.model_validate_json(resp)
|
|
39
|
+
return entity
|
|
40
|
+
else:
|
|
41
|
+
return response
|
|
42
|
+
|
|
43
|
+
elif response.status_code >= 400:
|
|
44
|
+
return handle_error_response(response)
|
|
45
|
+
else:
|
|
46
|
+
logger.warning(f"Unexpected return code: {response.status_code}")
|
|
47
|
+
return response
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def navigate(client: httpx.Client, link: Link,
|
|
51
|
+
parse_type: type[TEntity] = Entity) -> TEntity | ProblemDetails | Response:
|
|
52
|
+
return get_resource(client, link.href, link.type, parse_type)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def ensure_siren_response(response: TEntity | ProblemDetails | Response) -> TEntity:
|
|
56
|
+
if isinstance(response, ProblemDetails):
|
|
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}")
|
|
62
|
+
return response
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def upload_json(client: httpx.Client, action: Action, json_payload: Any,
|
|
66
|
+
filename: str) -> None | URL | ProblemDetails | Response:
|
|
67
|
+
return upload_binary(client, action, json.dumps(json_payload), filename, MediaTypes.APPLICATION_JSON)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def upload_binary(client: httpx.Client, action: Action, content: str | bytes | Iterable[bytes] | AsyncIterable[bytes],
|
|
71
|
+
filename: str,
|
|
72
|
+
mediatype: str = MediaTypes.OCTET_STREAM) -> None | URL | ProblemDetails | Response:
|
|
73
|
+
|
|
74
|
+
if isinstance(content, str):
|
|
75
|
+
payload = BytesIO(content.encode(encoding="utf-8"))
|
|
76
|
+
elif isinstance(content, bytes):
|
|
77
|
+
payload = BytesIO(content)
|
|
78
|
+
else:
|
|
79
|
+
# Fixme: iterable are not supported. Use the 'content' instead of the 'file' parameter of the request call?
|
|
80
|
+
raise NotImplemented('Iterables are not supported as payload (yet)! Convert to bytes or string.')
|
|
81
|
+
|
|
82
|
+
return upload_file(client, action, payload, filename, mediatype)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# for now no support for multi file upload
|
|
86
|
+
def upload_file(client: httpx.Client, action: Action, file: BinaryIO, filename: str,
|
|
87
|
+
mediatype: str = MediaTypes.OCTET_STREAM) -> None | URL | ProblemDetails | Response:
|
|
88
|
+
if action.type != MediaTypes.MULTIPART_FORM_DATA:
|
|
89
|
+
raise SirenException(
|
|
90
|
+
f"Action with upload requires type: {MediaTypes.MULTIPART_FORM_DATA} but found: {action.type}")
|
|
91
|
+
|
|
92
|
+
files = {'upload-file': (filename, file, mediatype)}
|
|
93
|
+
try:
|
|
94
|
+
response = client.request(method=action.method, url=action.href, files=files)
|
|
95
|
+
except httpx.RequestError as exc:
|
|
96
|
+
raise SirenException(f"Error from httpx while uploading data to: {action.href}") from exc
|
|
97
|
+
return handle_action_result(response)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def execute_action_on_entity(client: httpx.Client, entity: Entity, name: str, parameters: BaseModel | None = None):
|
|
101
|
+
action = entity.find_first_action_with_name(name)
|
|
102
|
+
if action is None:
|
|
103
|
+
raise SirenException(f"Entity does not contain expected action: {name}")
|
|
104
|
+
|
|
105
|
+
return execute_action(client, action, parameters)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def execute_action(client: httpx.Client, action: Action,
|
|
109
|
+
parameters: BaseModel | None = None) -> None | URL | ProblemDetails | Response:
|
|
110
|
+
if action.has_parameters() is False:
|
|
111
|
+
# no parameters required
|
|
112
|
+
if parameters is not None:
|
|
113
|
+
raise SirenException(f"Action requires no parameters but got some")
|
|
114
|
+
else:
|
|
115
|
+
# parameters required
|
|
116
|
+
if parameters is None:
|
|
117
|
+
raise SirenException(f"Action requires parameters but non provided")
|
|
118
|
+
|
|
119
|
+
action_parameters = None
|
|
120
|
+
if parameters is not None:
|
|
121
|
+
action_parameters = parameters.model_dump_json(by_alias=True, exclude_none=True)
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
response = client.request(
|
|
125
|
+
method=action.method,
|
|
126
|
+
url=action.href,
|
|
127
|
+
content=action_parameters,
|
|
128
|
+
headers={Headers.CONTENT_TYPE.value: MediaTypes.APPLICATION_JSON.value}
|
|
129
|
+
)
|
|
130
|
+
except httpx.RequestError as exc:
|
|
131
|
+
raise SirenException(f"Error from httpx while executing action: {action.href}") from exc
|
|
132
|
+
|
|
133
|
+
return handle_action_result(response)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def handle_action_result(response: Response) -> None | URL | ProblemDetails | Response:
|
|
137
|
+
if response.status_code == httpx.codes.OK:
|
|
138
|
+
return
|
|
139
|
+
if response.status_code == httpx.codes.CREATED:
|
|
140
|
+
location_header = response.headers.get(Headers.LOCATION_HEADER, None)
|
|
141
|
+
if location_header is None:
|
|
142
|
+
logger.warning(f"Got created response without location header")
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
return URL(location_header)
|
|
146
|
+
|
|
147
|
+
elif response.status_code >= 400:
|
|
148
|
+
return handle_error_response(response)
|
|
149
|
+
else:
|
|
150
|
+
logger.warning(f"Unexpected return code: {response.status_code}")
|
|
151
|
+
return response
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def handle_error_response(response: Response) -> ProblemDetails | Response:
|
|
155
|
+
content_type = response.headers.get(Headers.CONTENT_TYPE, '')
|
|
156
|
+
if content_type.startswith(MediaTypes.PROBLEM_DETAILS.value):
|
|
157
|
+
return ProblemDetails.model_validate(response.json())
|
|
158
|
+
else:
|
|
159
|
+
logger.warning(
|
|
160
|
+
f"Error case did not return media type: '{MediaTypes.PROBLEM_DETAILS.value}', "
|
|
161
|
+
f"got '{response.headers.get(Headers.CONTENT_TYPE, None)}' instead!")
|
|
162
|
+
return response
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def navigate_self(client: httpx.Client, entity: Entity) -> Entity | ProblemDetails | None:
|
|
166
|
+
if entity is None:
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
self_link = entity.find_first_link_with_relation(BaseRelations.SELF)
|
|
170
|
+
if self_link is None:
|
|
171
|
+
return None
|
|
172
|
+
|
|
173
|
+
return navigate(client, self_link)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import TypeVar, Type
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from hypermedia_client.core import Entity
|
|
8
|
+
from hypermedia_client.core.enterapi import enter_api
|
|
9
|
+
from hypermedia_client.core.hco.hco_base import Hco
|
|
10
|
+
from hypermedia_client.job_management.hcos.entrypoint_hco import EntryPointHco
|
|
11
|
+
from hypermedia_client.job_management.model.sirenentities import EntryPointEntity
|
|
12
|
+
import hypermedia_client.job_management
|
|
13
|
+
|
|
14
|
+
LOG = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
THco = TypeVar("THco", bound=Hco)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _version_match_major_minor(ver1: list[int], ver2: list[int]) -> bool:
|
|
20
|
+
return all([v1 == v2 for v1, v2 in zip(ver1[:2], ver2[:2])])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def enter_jma(
|
|
24
|
+
client: httpx.Client,
|
|
25
|
+
entrypoint_hco_type: Type[THco] = EntryPointHco,
|
|
26
|
+
entrypoint_entity_type: Type[Entity] = EntryPointEntity,
|
|
27
|
+
entrypoint: str = "api/EntryPoint",
|
|
28
|
+
) -> EntryPointHco:
|
|
29
|
+
entry_point_hco = enter_api(client, entrypoint_hco_type, entrypoint_entity_type, entrypoint)
|
|
30
|
+
|
|
31
|
+
info = entry_point_hco.info_link.navigate()
|
|
32
|
+
|
|
33
|
+
# Check for matching protocol versions
|
|
34
|
+
client_version = hypermedia_client.job_management.__jma_version__
|
|
35
|
+
jma_version = [int(i) for i in str.split(info.api_version, '.')]
|
|
36
|
+
if not _version_match_major_minor(jma_version, client_version):
|
|
37
|
+
LOG.warning(
|
|
38
|
+
f"Version mismatch between 'hypermedia_client' (v{'.'.join(map(str ,client_version))}) "
|
|
39
|
+
f"and 'JobManagementAPI' (v{'.'.join(map(str, jma_version))})! "
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return entry_point_hco
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .input_dataslot_hco import *
|
|
2
|
+
from .entrypoint_hco import *
|
|
3
|
+
from .info_hco import *
|
|
4
|
+
from .job_hco import *
|
|
5
|
+
from .job_query_result_hco import *
|
|
6
|
+
from .jobsroot_hco import *
|
|
7
|
+
from .processing_step_hco import *
|
|
8
|
+
from .processingstep_query_result_hco import *
|
|
9
|
+
from .processingsteproot_hco import *
|
|
10
|
+
from .workdata_hco import *
|
|
11
|
+
from .workdata_query_result_hco import *
|
|
12
|
+
from .workdataroot_hco import *
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from enum import StrEnum
|
|
2
|
+
from typing import Self
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from hypermedia_client.core.hco.hco_base import Hco
|
|
7
|
+
from hypermedia_client.core.hco.link_hco import LinkHco
|
|
8
|
+
from hypermedia_client.job_management.hcos.info_hco import InfoLink
|
|
9
|
+
from hypermedia_client.job_management.hcos.jobsroot_hco import JobsRootLink
|
|
10
|
+
from hypermedia_client.job_management.hcos.processingsteproot_hco import ProcessingStepsRootLink
|
|
11
|
+
from hypermedia_client.job_management.hcos.workdataroot_hco import WorkDataRootLink
|
|
12
|
+
from hypermedia_client.job_management.known_relations import Relations
|
|
13
|
+
from hypermedia_client.job_management.model.sirenentities import EntryPointEntity
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class EntryPointLink(LinkHco):
|
|
17
|
+
def navigate(self) -> 'EntryPointHco':
|
|
18
|
+
return EntryPointHco.from_entity(self._navigate_internal(EntryPointEntity), self._client)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class EntrypointRelations(StrEnum):
|
|
22
|
+
JOBS_ROOT = "JobsRoot"
|
|
23
|
+
WORKDATA_ROOT = "WorkDataRoot"
|
|
24
|
+
PROCESSINGSTEPS_ROOT = "ProcessingStepsRoot"
|
|
25
|
+
INFO = "Info"
|
|
26
|
+
ADMIN = "Admin"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EntryPointHco(Hco[EntryPointEntity]):
|
|
30
|
+
self_link: EntryPointLink
|
|
31
|
+
job_root_link: JobsRootLink
|
|
32
|
+
work_data_root_link: WorkDataRootLink
|
|
33
|
+
processing_step_root_link: ProcessingStepsRootLink
|
|
34
|
+
info_link: InfoLink
|
|
35
|
+
|
|
36
|
+
admin_link: LinkHco | None
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def from_entity(cls, entity: EntryPointEntity, client: httpx.Client) -> Self:
|
|
40
|
+
instance = cls(client, entity)
|
|
41
|
+
Hco.check_classes(instance._entity.class_, ["EntryPoint"])
|
|
42
|
+
|
|
43
|
+
instance.self_link = EntryPointLink.from_entity(
|
|
44
|
+
instance._client, instance._entity, Relations.SELF)
|
|
45
|
+
instance.info_link = InfoLink.from_entity(
|
|
46
|
+
instance._client, instance._entity, EntrypointRelations.INFO)
|
|
47
|
+
instance.job_root_link = JobsRootLink.from_entity(
|
|
48
|
+
instance._client, instance._entity, EntrypointRelations.JOBS_ROOT)
|
|
49
|
+
instance.work_data_root_link = WorkDataRootLink.from_entity(
|
|
50
|
+
instance._client, instance._entity, EntrypointRelations.WORKDATA_ROOT)
|
|
51
|
+
instance.processing_step_root_link = ProcessingStepsRootLink.from_entity(
|
|
52
|
+
instance._client, instance._entity, EntrypointRelations.PROCESSINGSTEPS_ROOT)
|
|
53
|
+
|
|
54
|
+
instance.admin_link = LinkHco.from_entity_optional(
|
|
55
|
+
instance._client, instance._entity, EntrypointRelations.ADMIN)
|
|
56
|
+
|
|
57
|
+
return instance
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from hypermedia_client.core.hco.hco_base import Hco, Property
|
|
6
|
+
from hypermedia_client.core.hco.link_hco import LinkHco
|
|
7
|
+
from hypermedia_client.job_management.hcos.user_hco import UserHco
|
|
8
|
+
from hypermedia_client.job_management.known_relations import Relations
|
|
9
|
+
from hypermedia_client.job_management.model.sirenentities import InfoEntity, UserEntity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InfoLink(LinkHco):
|
|
13
|
+
def navigate(self) -> "InfoHco":
|
|
14
|
+
return InfoHco.from_entity(self._navigate_internal(InfoEntity), self._client)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class InfoHco(Hco[InfoEntity]):
|
|
18
|
+
api_version: str = Property()
|
|
19
|
+
build_version: str = Property()
|
|
20
|
+
current_user: UserHco
|
|
21
|
+
used_storage_in_bytes: int = Property()
|
|
22
|
+
|
|
23
|
+
self_link: InfoLink
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_entity(cls, entity: InfoEntity, client: httpx.Client) -> Self:
|
|
27
|
+
instance = cls(client, entity)
|
|
28
|
+
|
|
29
|
+
Hco.check_classes(instance._entity.class_, ["Info"])
|
|
30
|
+
|
|
31
|
+
instance.self_link = InfoLink.from_entity(
|
|
32
|
+
instance._client, instance._entity, Relations.SELF
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
instance._extract_current_user()
|
|
36
|
+
|
|
37
|
+
return instance
|
|
38
|
+
|
|
39
|
+
def _extract_current_user(self):
|
|
40
|
+
user_entity = self._entity.find_first_entity_with_relation(
|
|
41
|
+
Relations.CURRENT_USER, UserEntity)
|
|
42
|
+
self.current_user = UserHco.from_entity(user_entity, self._client)
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
from hypermedia_client.core import Link, MediaTypes
|
|
6
|
+
from hypermedia_client.core.hco.action_hco import ActionHco
|
|
7
|
+
from hypermedia_client.core.hco.action_with_parameters_hco import ActionWithParametersHco
|
|
8
|
+
from hypermedia_client.core.hco.hco_base import Hco, Property
|
|
9
|
+
from hypermedia_client.core.hco.link_hco import LinkHco
|
|
10
|
+
from hypermedia_client.core.hco.upload_action_hco import UploadAction, UploadParameters
|
|
11
|
+
from hypermedia_client.job_management.hcos.workdata_hco import WorkDataLink, WorkDataHco
|
|
12
|
+
from hypermedia_client.job_management.known_relations import Relations
|
|
13
|
+
from hypermedia_client.job_management.model.open_api_generated import SelectWorkDataForDataSlotParameters, \
|
|
14
|
+
SelectWorkDataCollectionForDataSlotParameters
|
|
15
|
+
from hypermedia_client.job_management.model.sirenentities import InputDataSlotEntity, WorkDataEntity
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InputDataSlotLink(LinkHco):
|
|
19
|
+
def navigate(self) -> 'InputDataSlotHco':
|
|
20
|
+
return InputDataSlotHco.from_entity(self._navigate_internal(InputDataSlotEntity), self._client)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class InputDataSlotSelectWorkDataAction(ActionWithParametersHco[SelectWorkDataForDataSlotParameters]):
|
|
24
|
+
def execute(self, parameters: SelectWorkDataForDataSlotParameters):
|
|
25
|
+
self._execute(parameters)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class InputDataSlotSelectWorkDataCollectionAction(ActionWithParametersHco[SelectWorkDataCollectionForDataSlotParameters]):
|
|
29
|
+
def execute(self, parameters: SelectWorkDataCollectionForDataSlotParameters):
|
|
30
|
+
self._execute(parameters)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InputDataSlotUploadWorkDataAction(UploadAction):
|
|
34
|
+
def execute(self, parameters: UploadParameters) -> WorkDataLink:
|
|
35
|
+
url = self._upload(parameters)
|
|
36
|
+
link = Link.from_url(url, [str(Relations.CREATED_RESSOURCE)], "Uploaded workdata", MediaTypes.SIREN)
|
|
37
|
+
return WorkDataLink.from_link(self._client, link)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class InputDataSlotClearDataAction(ActionHco):
|
|
41
|
+
def execute(self):
|
|
42
|
+
self._execute_internal()
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class InputDataSlotHco(Hco[InputDataSlotEntity]):
|
|
46
|
+
is_configured: bool | None = Property()
|
|
47
|
+
title: str | None = Property()
|
|
48
|
+
description: str | None = Property()
|
|
49
|
+
media_type: str | None = Property()
|
|
50
|
+
selected_workdatas: list[WorkDataHco] | None
|
|
51
|
+
|
|
52
|
+
select_workdata_action: InputDataSlotSelectWorkDataAction | None
|
|
53
|
+
select_workdata_collection_action: InputDataSlotSelectWorkDataCollectionAction | None
|
|
54
|
+
clear_workdata_action: InputDataSlotClearDataAction | None
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def from_entity(cls, entity: InputDataSlotEntity, client: httpx.Client) -> Self:
|
|
58
|
+
instance = cls(client, entity)
|
|
59
|
+
Hco.check_classes(instance._entity.class_, ["InputDataSlot"])
|
|
60
|
+
|
|
61
|
+
# actions
|
|
62
|
+
instance.select_workdata_action = InputDataSlotSelectWorkDataAction.from_entity_optional(
|
|
63
|
+
client, instance._entity, "SelectWorkData")
|
|
64
|
+
instance.select_workdata_collection_action = InputDataSlotSelectWorkDataCollectionAction.from_entity_optional(
|
|
65
|
+
client, instance._entity, "SelectWorkDataCollection")
|
|
66
|
+
instance.clear_workdata_action = InputDataSlotClearDataAction.from_entity_optional(
|
|
67
|
+
client, instance._entity, "Clear")
|
|
68
|
+
|
|
69
|
+
instance._extract_workdata()
|
|
70
|
+
|
|
71
|
+
return instance
|
|
72
|
+
|
|
73
|
+
def _extract_workdata(self):
|
|
74
|
+
self.selected_workdatas = []
|
|
75
|
+
|
|
76
|
+
workdatas: list[WorkDataEntity] = self._entity.find_all_entities_with_relation(Relations.SELECTED,
|
|
77
|
+
WorkDataEntity)
|
|
78
|
+
if not workdatas:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
self.selected_workdatas = list[WorkDataHco](
|
|
82
|
+
WorkDataHco.from_entity(workdata, self._client) for workdata in workdatas)
|