mdtpy 25.2.3__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.
- mdtpy/__init__.py +4 -0
- mdtpy/cli.py +160 -0
- mdtpy/client/__init__.py +7 -0
- mdtpy/client/http_client.py +66 -0
- mdtpy/client/http_fa3st_client.py +66 -0
- mdtpy/client/http_instance_client.py +316 -0
- mdtpy/client/http_registry_client.py +84 -0
- mdtpy/client/http_repository_client.py +100 -0
- mdtpy/client/http_service_client.py +346 -0
- mdtpy/client/utils.py +66 -0
- mdtpy/impl/__init__.py +0 -0
- mdtpy/impl/json_deserializer.py +136 -0
- mdtpy/model/__init__.py +13 -0
- mdtpy/model/aas_model.py +732 -0
- mdtpy/model/aas_registry.py +76 -0
- mdtpy/model/aas_repository.py +47 -0
- mdtpy/model/aas_service.py +190 -0
- mdtpy/model/exceptions.py +56 -0
- mdtpy/model/ksx9101.py +85 -0
- mdtpy/model/mdt.py +479 -0
- mdtpy/model/reference.py +184 -0
- mdtpy/model/value.py +96 -0
- mdtpy/types.py +82 -0
- mdtpy-25.2.3.dist-info/METADATA +12 -0
- mdtpy-25.2.3.dist-info/RECORD +27 -0
- mdtpy-25.2.3.dist-info/WHEEL +5 -0
- mdtpy-25.2.3.dist-info/top_level.txt +1 -0
mdtpy/__init__.py
ADDED
mdtpy/cli.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional
|
|
5
|
+
import subprocess
|
|
6
|
+
|
|
7
|
+
from mdt.types import *
|
|
8
|
+
from mdt.types import Parameter, Input, Output, ElementReference
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
MDT_OPERATION_SERVER_ENDPOINT = "http://localhost:12987"
|
|
12
|
+
LOG_LEVEL:Optional[str] = None
|
|
13
|
+
CLIENT_CONFIG_PATH:Optional[str] = None
|
|
14
|
+
|
|
15
|
+
def list_instances(filter:Optional[str]=None) -> list[str]:
|
|
16
|
+
filter_opt = f" --filter {filter}" if filter else ""
|
|
17
|
+
loglevel_str = f" --loglevel {LOG_LEVEL}" if LOG_LEVEL else ""
|
|
18
|
+
client_conf_opt = f" --client_conf {CLIENT_CONFIG_PATH}" if CLIENT_CONFIG_PATH else ""
|
|
19
|
+
cmd:str = f"mdt list instances {filter_opt}{loglevel_str}{client_conf_opt}"
|
|
20
|
+
result = subprocess.run(cmd, shell=True, capture_output=True)
|
|
21
|
+
if result.returncode != 0:
|
|
22
|
+
raise MDTCliError(result.stderr)
|
|
23
|
+
return result.stdout.decode('utf-8').splitlines()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def add_instance(id:str, model_path:str, conf_path:str,
|
|
27
|
+
jar_path:Optional[str]=None,
|
|
28
|
+
port:Optional[int]=None,
|
|
29
|
+
logger:Optional[str]=None,
|
|
30
|
+
endpoint:Optional[str]=None):
|
|
31
|
+
jar_path_str = f" --jar {jar_path}" if jar_path else ""
|
|
32
|
+
port_str = f" --port {port}" if port else ""
|
|
33
|
+
logger_str = f" --logger {logger}" if logger else ""
|
|
34
|
+
ep_str = f" --endpoint {endpoint}" if endpoint else ""
|
|
35
|
+
cmd:str = f"mdt add {id} -m {model_path} -c {conf_path}{jar_path_str}{port_str} " \
|
|
36
|
+
f"{logger_str}{ep_str}"
|
|
37
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, check=True, encoding='utf-8')
|
|
38
|
+
if result.returncode != 0:
|
|
39
|
+
raise MDTCliError(result.stderr)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def remove_instance(id:str, endpoint:Optional[str]=None):
|
|
43
|
+
ep_str = f" --endpoint {endpoint}" if endpoint else ""
|
|
44
|
+
cmd:str = f"mdt remove {id}{ep_str}"
|
|
45
|
+
result = subprocess.run(cmd, shell=True, capture_output=True, check=True, encoding='utf-8')
|
|
46
|
+
if result.returncode != 0:
|
|
47
|
+
raise MDTCliError(result.stderr)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def start_instance(id:str, all:bool=False, recursive:bool=False, nowait:bool=False, thread_count:int=1,
|
|
51
|
+
poll_interval:str="1s", timeout:Optional[str]=None):
|
|
52
|
+
all_opt = f" --all" if all else ""
|
|
53
|
+
recursive_opt = f" --recursive" if recursive else ""
|
|
54
|
+
thread_count_opt = f" --nthreads {thread_count}" if all else ""
|
|
55
|
+
nowait_opt = f" --nowait" if nowait else ""
|
|
56
|
+
poll_interval_pot = f" --poll {poll_interval}"
|
|
57
|
+
timeout_opt = f" --timeout {timeout}" if all else ""
|
|
58
|
+
loglevel_str = f" --loglevel {LOG_LEVEL}" if LOG_LEVEL else ""
|
|
59
|
+
client_conf_opt = f" --client_conf {CLIENT_CONFIG_PATH}" if CLIENT_CONFIG_PATH else ""
|
|
60
|
+
cmd:str = f"mdt start {id} {all_opt}{recursive_opt}{thread_count_opt}{nowait_opt}" \
|
|
61
|
+
f"{poll_interval_pot}{timeout_opt}{loglevel_str}{client_conf_opt}"
|
|
62
|
+
result = subprocess.run(cmd, shell=True, capture_output=True)
|
|
63
|
+
if result.returncode != 0:
|
|
64
|
+
raise MDTCliError(result.stderr)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def stop_instance(id:str, all:bool=False, recursive:bool=False, nowait:bool=False):
|
|
68
|
+
all_opt = f" --all" if all else ""
|
|
69
|
+
recursive_opt = f" --recursive" if recursive else ""
|
|
70
|
+
nowait_opt = f" --nowait" if nowait else ""
|
|
71
|
+
loglevel_str = f" --loglevel {LOG_LEVEL}" if LOG_LEVEL else ""
|
|
72
|
+
client_conf_opt = f" --client_conf {CLIENT_CONFIG_PATH}" if CLIENT_CONFIG_PATH else ""
|
|
73
|
+
cmd:str = f"mdt stop {id} {all_opt}{recursive_opt}{nowait_opt}{loglevel_str}{client_conf_opt}"
|
|
74
|
+
result = subprocess.run(cmd, shell=True, capture_output=True)
|
|
75
|
+
if result.returncode != 0:
|
|
76
|
+
raise MDTCliError(result.stderr)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_element(ref:Parameter|Input|Output|ElementReference, output:str='value'):
|
|
80
|
+
cmd:str = f"mdt get element {ref} -o {output}"
|
|
81
|
+
result = subprocess.run(cmd, shell=True, capture_output=True)
|
|
82
|
+
if result.returncode != 0:
|
|
83
|
+
raise MDTCliError(result.stderr)
|
|
84
|
+
return result.stdout.decode('utf-8')
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def set_element(ref:Parameter|Input|Output|ElementReference, value:str|File):
|
|
88
|
+
value_spec = None
|
|
89
|
+
if isinstance(value, str):
|
|
90
|
+
value_spec = f" --value {value}"
|
|
91
|
+
elif isinstance(value, File):
|
|
92
|
+
value_spec = f" --file {value.file_path}"
|
|
93
|
+
if value.path:
|
|
94
|
+
value_spec = f"{value_spec} --path {value.path}"
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(f'Unexpected value: f{value}')
|
|
97
|
+
|
|
98
|
+
loglevel_str = f" --loglevel {LOG_LEVEL}" if LOG_LEVEL else ""
|
|
99
|
+
client_conf_opt = f" --client_conf {CLIENT_CONFIG_PATH}" if CLIENT_CONFIG_PATH else ""
|
|
100
|
+
cmd:str = f"mdt set {ref}{value_spec} {loglevel_str}{client_conf_opt}"
|
|
101
|
+
result = subprocess.run(cmd, shell=True, capture_output=True)
|
|
102
|
+
if result.returncode != 0:
|
|
103
|
+
raise MDTCliError(result.stderr)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def copy(src_ref:Parameter|Input|Output|ElementReference, tar_ref:Parameter|Input|Output|ElementReference):
|
|
107
|
+
loglevel_str = f" --loglevel {LOG_LEVEL}" if LOG_LEVEL else ""
|
|
108
|
+
client_conf_opt = f" --client_conf {CLIENT_CONFIG_PATH}" if CLIENT_CONFIG_PATH else ""
|
|
109
|
+
cmd:str = f"mdt copy {src_ref} {tar_ref}{loglevel_str}{client_conf_opt}"
|
|
110
|
+
result = subprocess.run(cmd, shell=True, capture_output=True)
|
|
111
|
+
if result.returncode != 0:
|
|
112
|
+
raise MDTCliError(result.stderr)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def run_program(program_path:str):
|
|
116
|
+
loglevel_str = f" --loglevel {LOG_LEVEL}" if LOG_LEVEL else ""
|
|
117
|
+
client_conf_opt = f" --client_conf {CLIENT_CONFIG_PATH}" if CLIENT_CONFIG_PATH else ""
|
|
118
|
+
cmd:str = f"mdt run program {program_path} {loglevel_str}{client_conf_opt}"
|
|
119
|
+
result = subprocess.run(cmd, shell=True, capture_output=True)
|
|
120
|
+
if result.returncode != 0:
|
|
121
|
+
raise MDTCliError(result.stderr)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def replace_in_arg(arg_name:str):
|
|
125
|
+
return arg_name.replace('in_', 'in.')
|
|
126
|
+
def replace_out_arg(arg_name:str):
|
|
127
|
+
return arg_name.replace('out_', 'out.')
|
|
128
|
+
def run_http(op_id:str, server:str, poll_interval:Optional[str]=None, timeout:Optional[str]=None, **kwargs):
|
|
129
|
+
loglevel_str = f" --loglevel {LOG_LEVEL}" if LOG_LEVEL else ""
|
|
130
|
+
client_conf_opt = f" --client_conf {CLIENT_CONFIG_PATH}" if CLIENT_CONFIG_PATH else ""
|
|
131
|
+
poll_interval_opt = f" --poll {poll_interval}" if poll_interval else ""
|
|
132
|
+
timeout_opt = f" --timeout {timeout}" if timeout else ""
|
|
133
|
+
extra_in_args = ' '.join([f'--{replace_in_arg(key)} {value}' for key, value in kwargs.items() if key.startswith('in_')])
|
|
134
|
+
extra_out_args = ' '.join([f'--{replace_out_arg(key)} {value}' for key, value in kwargs.items() if key.startswith('out_')])
|
|
135
|
+
cmd:str = f"mdt run http --server {server} --id {op_id} {poll_interval_opt}{timeout_opt}" \
|
|
136
|
+
f"{loglevel_str}{client_conf_opt} {extra_in_args} {extra_out_args}"
|
|
137
|
+
result = subprocess.run(cmd, shell=True, capture_output=True)
|
|
138
|
+
if result.returncode != 0:
|
|
139
|
+
raise MDTCliError(result.stderr)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
import os
|
|
143
|
+
def main():
|
|
144
|
+
start_instance('heater')
|
|
145
|
+
# set_value('param:Test/0', '15')
|
|
146
|
+
|
|
147
|
+
# os.chdir('/home/kwlee/mdt/models/innercase')
|
|
148
|
+
# set_file('param:inspector/UpperIlluminanceImage', file='Innercase07-1.jpg')
|
|
149
|
+
|
|
150
|
+
# os.chdir('/home/kwlee/mdt/models/innercase')
|
|
151
|
+
# # run_program('program.json', logger='info')
|
|
152
|
+
# run_http(op_id='test', submodel='Test/Simulation', timeout='5m', logger='info')
|
|
153
|
+
|
|
154
|
+
if __name__ == '__main__':
|
|
155
|
+
main()
|
|
156
|
+
|
|
157
|
+
# mdt run http --server http://localhost:12987 --id inspector/UpdateDefectList \
|
|
158
|
+
# --in.Defect arg:inspector/ThicknessInspection/out/Defect --in.DefectList param:inspector/DefectList --out.DefectList param:inspector/DefectList
|
|
159
|
+
|
|
160
|
+
# mdt run http --server http://localhost:12987 --id inspector/UpdateDefectList
|
mdtpy/client/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from mdtpy.model import MDTException, RemoteError
|
|
9
|
+
from ..model.aas_model import Endpoint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def to_base64_string(src:str) -> str:
|
|
13
|
+
return base64.urlsafe_b64encode(bytes(src, 'utf-8')).decode('ascii')
|
|
14
|
+
|
|
15
|
+
def parse_none_response(resp:requests.Response) -> None:
|
|
16
|
+
if resp.status_code >= 200 and resp.status_code < 300:
|
|
17
|
+
return
|
|
18
|
+
else:
|
|
19
|
+
raise to_exception(resp)
|
|
20
|
+
|
|
21
|
+
def parse_response(result_cls, resp:requests.Response):
|
|
22
|
+
if resp.status_code >= 200 and resp.status_code < 300:
|
|
23
|
+
return result_cls.from_json(resp.text)
|
|
24
|
+
else:
|
|
25
|
+
raise to_exception(resp)
|
|
26
|
+
|
|
27
|
+
def parse_list_response(result_cls, resp:requests.Response):
|
|
28
|
+
if resp.status_code >= 200 and resp.status_code < 300:
|
|
29
|
+
return [result_cls.from_dict(descElm) for descElm in resp.json()]
|
|
30
|
+
else:
|
|
31
|
+
raise to_exception(resp)
|
|
32
|
+
|
|
33
|
+
def to_exception(resp:requests.Response) -> MDTException:
|
|
34
|
+
json_obj = resp.json()
|
|
35
|
+
|
|
36
|
+
if 'messages' in json_obj:
|
|
37
|
+
message = json_obj['messages'][0]
|
|
38
|
+
return RemoteError(message['text'])
|
|
39
|
+
elif 'code' in json_obj:
|
|
40
|
+
code = json_obj['code']
|
|
41
|
+
if code == 'java.lang.IllegalArgumentException':
|
|
42
|
+
raise RemoteError(json_obj['message'])
|
|
43
|
+
elif code == 'utils.InternalException':
|
|
44
|
+
raise RemoteError(json_obj['message'])
|
|
45
|
+
elif code == 'java.lang.NullPointerException':
|
|
46
|
+
raise RemoteError(f"code={json_obj['code']}, message={json_obj['message']}")
|
|
47
|
+
elif code == 'org.springframework.web.servlet.resource.NoResourceFoundException':
|
|
48
|
+
raise RemoteError(json_obj['text'])
|
|
49
|
+
elif code == 'org.springframework.web.HttpRequestMethodNotSupportedException':
|
|
50
|
+
raise RemoteError(json_obj['text'])
|
|
51
|
+
paths = code.split('.')
|
|
52
|
+
|
|
53
|
+
from importlib import import_module
|
|
54
|
+
moduleName = '.'.join(paths[:-1])
|
|
55
|
+
module = import_module(moduleName)
|
|
56
|
+
exception_cls = getattr(module, paths[-1])
|
|
57
|
+
return exception_cls(json_obj['text'])
|
|
58
|
+
|
|
59
|
+
def extract_href(endpoints:list[Endpoint]) -> Optional[str]:
|
|
60
|
+
if len(endpoints) == 0:
|
|
61
|
+
return None
|
|
62
|
+
href = endpoints[0].protocolInformation.href
|
|
63
|
+
if href == None or len(href) == 0:
|
|
64
|
+
return None
|
|
65
|
+
else:
|
|
66
|
+
return href
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Any
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import requests
|
|
8
|
+
import json
|
|
9
|
+
|
|
10
|
+
from ..model import MDTException, ResourceNotFoundError, InternalError
|
|
11
|
+
from .http_client import to_exception, parse_none_response
|
|
12
|
+
from ..impl import json_deserializer
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class Message:
|
|
16
|
+
messageType: str
|
|
17
|
+
text: str
|
|
18
|
+
code: str
|
|
19
|
+
timestamp: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HttpFa3stClient:
|
|
23
|
+
"""
|
|
24
|
+
A client class for FA3ST API that handles HTTP requests and responses.
|
|
25
|
+
-------
|
|
26
|
+
to_base64_string(id: str) -> str
|
|
27
|
+
Encodes the given string ID to a base64 string.
|
|
28
|
+
parse_none_response(resp: requests.Response) -> None
|
|
29
|
+
Parses a response that is expected to have no content.
|
|
30
|
+
parse_response(result_cls, resp: requests.Response)
|
|
31
|
+
Parses the HTTP response based on the status code and expected result class.
|
|
32
|
+
to_exception(resp: requests.Response) -> MDTException
|
|
33
|
+
Converts an HTTP response to an appropriate exception based on the response content.
|
|
34
|
+
"""
|
|
35
|
+
def to_base64_string(self, id:str) -> str:
|
|
36
|
+
return base64.b64encode(id.encode('UTF-8')).decode('ascii')
|
|
37
|
+
|
|
38
|
+
def parse_none_response(self, resp:requests.Response) -> None:
|
|
39
|
+
return parse_none_response(resp)
|
|
40
|
+
|
|
41
|
+
def parse_response(self, result_cls, resp:requests.Response):
|
|
42
|
+
if resp.status_code == 204:
|
|
43
|
+
return None
|
|
44
|
+
elif resp.status_code >= 200 and resp.status_code < 300:
|
|
45
|
+
if result_cls == bytes:
|
|
46
|
+
content_type = resp.headers['content-type']
|
|
47
|
+
content = resp.content
|
|
48
|
+
return content_type, content
|
|
49
|
+
|
|
50
|
+
resp_json = json.loads(resp.text)
|
|
51
|
+
if 'result' in resp_json:
|
|
52
|
+
resp_json = resp_json['result'][0]
|
|
53
|
+
return json_deserializer.read_resource(resp_json)
|
|
54
|
+
else:
|
|
55
|
+
raise to_exception(resp)
|
|
56
|
+
|
|
57
|
+
def to_exception(self, resp:requests.Response) -> MDTException:
|
|
58
|
+
json_obj = resp.json()
|
|
59
|
+
message = Message(**json_obj['messages'][0])
|
|
60
|
+
if message.text.startswith("Resource not found"):
|
|
61
|
+
details = message.text[41:-1]
|
|
62
|
+
return ResourceNotFoundError.create("ModelRef", details)
|
|
63
|
+
elif message.text.startswith('error parsing body'):
|
|
64
|
+
return InternalError('JSON parsing failed')
|
|
65
|
+
else:
|
|
66
|
+
return MDTException(message.text)
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional, Generator, cast
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from mdtpy.model import MDTInstanceManager, MDTInstance, MDTInstanceCollection, InstanceDescriptor, InstanceSubmodelDescriptor, \
|
|
9
|
+
MDTInstanceStatus, SubmodelServiceCollection, OperationSubmodelServiceCollection, MDT_SEMANTIC_ID, \
|
|
10
|
+
InvalidResourceStateError, ResourceNotFoundError
|
|
11
|
+
|
|
12
|
+
from .utils import StatusPoller
|
|
13
|
+
from .http_registry_client import HttpAssetAdministrationShellRegistryClient, HttpSubmodelRegistryClient
|
|
14
|
+
from .http_service_client import HttpAssetAdministrationShellServiceClient, HttpSubmodelServiceClient
|
|
15
|
+
from .http_repository_client import *
|
|
16
|
+
from .http_client import to_base64_string, parse_none_response, parse_response, parse_list_response, extract_href
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def connect(host:str='localhost', port:int=12985, path:str='/instance-manager') -> HttpMDTManagerClient:
|
|
20
|
+
endpoint = f"http://{host}:{port}{path}"
|
|
21
|
+
return HttpMDTManagerClient(endpoint)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HttpMDTManagerClient(MDTInstanceManager):
|
|
25
|
+
def __init__(self, endpoint:str) -> None:
|
|
26
|
+
super().__init__()
|
|
27
|
+
|
|
28
|
+
self.endpoint = endpoint
|
|
29
|
+
self._instances = HttpInstanceCollection(self, endpoint)
|
|
30
|
+
self.endpoint_base = '/'.join(endpoint.split('/')[:-1])
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def instances(self) -> HttpInstanceCollection:
|
|
34
|
+
return self._instances
|
|
35
|
+
|
|
36
|
+
def getAssetAdministrationShellRegistry(self) -> HttpAssetAdministrationShellRegistryClient:
|
|
37
|
+
return HttpAssetAdministrationShellRegistryClient(self.endpoint_base + "/shell-registry/shell-descriptors")
|
|
38
|
+
|
|
39
|
+
def getSubmodelRegistry(self) -> HttpSubmodelRegistryClient:
|
|
40
|
+
return HttpSubmodelRegistryClient(self.endpoint_base + "/shell-registry/submodel-descriptors")
|
|
41
|
+
|
|
42
|
+
def getAssetAdministrationShellService(self, aasId:str) -> HttpAssetAdministrationShellServiceClient:
|
|
43
|
+
eps = self.getAssetAdministrationShellRegistry() \
|
|
44
|
+
.getAssetAdministrationShellDescriptorById(aasId) \
|
|
45
|
+
.endpoints
|
|
46
|
+
href = extract_href(eps)
|
|
47
|
+
if href:
|
|
48
|
+
return HttpAssetAdministrationShellServiceClient(href)
|
|
49
|
+
else:
|
|
50
|
+
raise InvalidResourceStateError.create("AssetAdministrationShell", f"id={aasId}")
|
|
51
|
+
|
|
52
|
+
def getSubmodelService(self, submodelId:str, serviceClass=None) -> SubmodelService:
|
|
53
|
+
eps = self.getSubmodelRegistry() \
|
|
54
|
+
.getSubmodelDescriptorById(submodelId) \
|
|
55
|
+
.endpoints
|
|
56
|
+
href = extract_href(eps)
|
|
57
|
+
if href:
|
|
58
|
+
return serviceClass(href) if serviceClass else HttpSubmodelServiceClient(href)
|
|
59
|
+
else:
|
|
60
|
+
raise InvalidResourceStateError.create("Submodel", f"id={submodelId}")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HttpInstanceCollection(MDTInstanceCollection):
|
|
64
|
+
def __init__(self, inst_mgr:HttpMDTManagerClient, endpoint:str):
|
|
65
|
+
self.url_prefix = f"{endpoint}/instances"
|
|
66
|
+
self.instance_manager = inst_mgr
|
|
67
|
+
|
|
68
|
+
def __bool__(self):
|
|
69
|
+
resp = requests.get(self.url_prefix)
|
|
70
|
+
return len(parse_list_response(InstanceDescriptor, resp)) > 0
|
|
71
|
+
|
|
72
|
+
def __len__(self):
|
|
73
|
+
resp = requests.get(self.url_prefix)
|
|
74
|
+
return len(parse_list_response(InstanceDescriptor, resp))
|
|
75
|
+
|
|
76
|
+
def __iter__(self) -> Iterator[HttpInstanceClient]:
|
|
77
|
+
resp = requests.get(self.url_prefix)
|
|
78
|
+
inst_desc_list = parse_list_response(InstanceDescriptor, resp)
|
|
79
|
+
return iter(HttpInstanceClient(self, self.instance_manager, inst_desc) for inst_desc in inst_desc_list)
|
|
80
|
+
|
|
81
|
+
def __contains__(self, key:str) -> bool:
|
|
82
|
+
url = f'{self.url_prefix}/{key}'
|
|
83
|
+
resp = requests.get(url)
|
|
84
|
+
return resp.status_code == 200
|
|
85
|
+
|
|
86
|
+
def __getitem__(self, key:str) -> HttpInstanceClient:
|
|
87
|
+
url = f'{self.url_prefix}/{key}'
|
|
88
|
+
resp = requests.get(url)
|
|
89
|
+
inst_desc = parse_response(InstanceDescriptor, resp)
|
|
90
|
+
return HttpInstanceClient(self.instance_manager, self.url_prefix, inst_desc)
|
|
91
|
+
|
|
92
|
+
def __setitem__(self, key:str, value:HttpInstanceClient) -> None:
|
|
93
|
+
raise NotImplementedError('HttpInstanceCollection does not support set operation')
|
|
94
|
+
|
|
95
|
+
def __delitem__(self, key:str) -> None:
|
|
96
|
+
url = f'{self.url_prefix}/{key}'
|
|
97
|
+
resp = requests.delete(url)
|
|
98
|
+
parse_none_response(resp)
|
|
99
|
+
|
|
100
|
+
def find(self, condition:str) -> Generator[HttpInstanceClient, None, None]:
|
|
101
|
+
resp = requests.get(self.url_prefix, params={'filter': f"{condition}"})
|
|
102
|
+
inst_desc_list = parse_list_response(InstanceDescriptor, resp)
|
|
103
|
+
return (HttpInstanceClient(self, inst_desc) for inst_desc in inst_desc_list)
|
|
104
|
+
|
|
105
|
+
def add(self, id:str, port:int, inst_dir:str) -> HttpInstanceClient:
|
|
106
|
+
import shutil
|
|
107
|
+
shutil.make_archive(inst_dir, 'zip', inst_dir)
|
|
108
|
+
zipped_file = f'{inst_dir}.zip'
|
|
109
|
+
|
|
110
|
+
from requests_toolbelt.multipart.encoder import MultipartEncoder
|
|
111
|
+
m = MultipartEncoder(
|
|
112
|
+
fields = {
|
|
113
|
+
'id': id,
|
|
114
|
+
'port': str(port),
|
|
115
|
+
'bundle': ('filename', open(zipped_file, 'rb'), 'application/zip')
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
resp = requests.post(self.url_prefix, data=m, headers={'Content-Type': m.content_type}, verify=False)
|
|
119
|
+
inst_desc = parse_response(InstanceDescriptor, resp)
|
|
120
|
+
return HttpInstanceClient(self, inst_desc)
|
|
121
|
+
|
|
122
|
+
def remove(self, id:str) -> None:
|
|
123
|
+
url = f'{self.url_prefix}/{id}'
|
|
124
|
+
resp = requests.delete(url)
|
|
125
|
+
return parse_none_response(resp)
|
|
126
|
+
|
|
127
|
+
def remove_all(self) -> None:
|
|
128
|
+
url = f"{self.url_prefix}"
|
|
129
|
+
resp = requests.delete(url)
|
|
130
|
+
parse_none_response(resp)
|
|
131
|
+
|
|
132
|
+
def __repr__(self):
|
|
133
|
+
return 'HttpMDTInstances(url={self.url_prefix})'
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class InstanceStartPoller(StatusPoller):
|
|
137
|
+
def __init__(self, status_url:str, init_desc:Optional[InstanceDescriptor]=None,
|
|
138
|
+
poll_interval:float=1.0, timeout:Optional[float]=None) -> None:
|
|
139
|
+
super().__init__(poll_interval=poll_interval, timeout=timeout)
|
|
140
|
+
self.status_url = status_url
|
|
141
|
+
self.desc = init_desc
|
|
142
|
+
|
|
143
|
+
def check_done(self) -> bool:
|
|
144
|
+
if self.desc.status == MDTInstanceStatus.STARTING.name:
|
|
145
|
+
resp = requests.get(self.status_url)
|
|
146
|
+
self.desc = parse_response(InstanceDescriptor, resp)
|
|
147
|
+
return self.desc.status != MDTInstanceStatus.STARTING.name
|
|
148
|
+
else:
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
class InstanceStopPoller(StatusPoller):
|
|
152
|
+
def __init__(self, status_url:str, init_desc:Optional[InstanceDescriptor]=None,
|
|
153
|
+
poll_interval:float=1.0, timeout:Optional[float]=None) -> None:
|
|
154
|
+
super().__init__(poll_interval=poll_interval, timeout=timeout)
|
|
155
|
+
self.status_url = status_url
|
|
156
|
+
self.desc = init_desc
|
|
157
|
+
|
|
158
|
+
def check_done(self) -> bool:
|
|
159
|
+
if self.desc.status == MDTInstanceStatus.STOPPING.name:
|
|
160
|
+
resp = requests.get(self.status_url)
|
|
161
|
+
self.desc = parse_response(InstanceDescriptor, resp)
|
|
162
|
+
return self.desc.status != MDTInstanceStatus.STOPPING.name
|
|
163
|
+
else:
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class HttpInstanceClient(MDTInstance):
|
|
168
|
+
def __init__(self, instance_manager:HttpMDTManagerClient, base_url:str, descriptor:InstanceDescriptor) -> None:
|
|
169
|
+
super().__init__()
|
|
170
|
+
|
|
171
|
+
self.instance_manager = instance_manager
|
|
172
|
+
self.descriptor = descriptor
|
|
173
|
+
self.base_url = base_url
|
|
174
|
+
self._submodels = HttpSubmodelServiceCollection(self)
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def id(self) -> str:
|
|
178
|
+
return self.descriptor.id
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def aasId(self) -> str:
|
|
182
|
+
return self.descriptor.aasId
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def aasIdShort(self) -> Optional[str]:
|
|
186
|
+
return self.descriptor.aasIdShort
|
|
187
|
+
|
|
188
|
+
@property
|
|
189
|
+
def status(self) -> MDTInstanceStatus:
|
|
190
|
+
return MDTInstanceStatus[self.descriptor.status]
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def serviceEndpoint(self) -> Optional[str]:
|
|
194
|
+
return self.descriptor.baseEndpoint
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def shell(self) -> HttpAssetAdministrationShellServiceClient:
|
|
198
|
+
return self.instance_manager.getAssetAdministrationShellService(self.aasId)
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def submodels(self) -> HttpSubmodelServiceCollection:
|
|
202
|
+
return self._submodels
|
|
203
|
+
|
|
204
|
+
def start(self, nowait=False) -> None:
|
|
205
|
+
url = f"{self.base_url}/{self.id}/start"
|
|
206
|
+
resp = requests.put(url, data="")
|
|
207
|
+
self.descriptor = parse_response(InstanceDescriptor, resp)
|
|
208
|
+
if nowait:
|
|
209
|
+
if self.descriptor.status != MDTInstanceStatus.STARTING.name and MDTInstanceStatus.RUNNING.name:
|
|
210
|
+
raise InvalidResourceStateError.create(f"Failed to start MDTInstance: id={self.id}")
|
|
211
|
+
else:
|
|
212
|
+
poller = InstanceStartPoller(f"{self.base_url}/{self.id}", init_desc=self.descriptor)
|
|
213
|
+
poller.wait_for_done()
|
|
214
|
+
self.descriptor = poller.desc
|
|
215
|
+
if self.descriptor.status != MDTInstanceStatus.RUNNING.name:
|
|
216
|
+
raise InvalidResourceStateError.create(f"Failed to start MDTInstance: id={self.id}")
|
|
217
|
+
|
|
218
|
+
def stop(self, nowait=False) -> None:
|
|
219
|
+
url = f"{self.base_url}/{self.id}/stop"
|
|
220
|
+
resp = requests.put(url, data="")
|
|
221
|
+
self.descriptor = parse_response(InstanceDescriptor, resp)
|
|
222
|
+
if nowait:
|
|
223
|
+
if self.descriptor.status != MDTInstanceStatus.STOPPING.name and MDTInstanceStatus.STOPPED.name:
|
|
224
|
+
raise InvalidResourceStateError.create(f"Failed to stop MDTInstance: id={self.id}")
|
|
225
|
+
else:
|
|
226
|
+
poller = InstanceStopPoller(f"{self.base_url}/{self.id}", init_desc=self.descriptor)
|
|
227
|
+
poller.wait_for_done()
|
|
228
|
+
self.descriptor = poller.desc
|
|
229
|
+
if self.descriptor.status != MDTInstanceStatus.STOPPED.name:
|
|
230
|
+
raise InvalidResourceStateError.create(f"Failed to stop MDTInstance: id={self.id}")
|
|
231
|
+
|
|
232
|
+
@property
|
|
233
|
+
def parameters(self) -> ElementReferenceCollection:
|
|
234
|
+
for sm_svc in self.submodels:
|
|
235
|
+
if sm_svc.semanticId == 'https://etri.re.kr/mdt/Submodel/Data/1/1':
|
|
236
|
+
return cast(DataSubmodelServiceClient, sm_svc).parameters
|
|
237
|
+
raise ResourceNotFoundError.create("Data Submodel", 'semanticId=https://etri.re.kr/mdt/Submodel/Data/1/1')
|
|
238
|
+
|
|
239
|
+
@property
|
|
240
|
+
def operations(self) -> OperationSubmodelServiceCollection:
|
|
241
|
+
return OperationSubmodelServiceCollection(self.submodels)
|
|
242
|
+
|
|
243
|
+
def __eq__(self, value: object) -> bool:
|
|
244
|
+
if isinstance(value, MDTInstance):
|
|
245
|
+
return self.id == value.id
|
|
246
|
+
else:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
def __repr__(self) -> str:
|
|
250
|
+
return f"MDTInstance[id={self.descriptor.id}, aas-id={self.descriptor.aasId}, " \
|
|
251
|
+
f"aas-id-short={self.descriptor.aasIdShort}, status={self.status}]"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class HttpSubmodelServiceCollection(SubmodelServiceCollection):
|
|
255
|
+
def __init__(self, instance:HttpInstanceClient) -> None:
|
|
256
|
+
super().__init__()
|
|
257
|
+
self.instance = instance
|
|
258
|
+
|
|
259
|
+
def __iter__(self) -> Iterator[SubmodelService]:
|
|
260
|
+
return iter(self.createSubmodelService(sm_desc) for sm_desc in self.instance.descriptor.submodels)
|
|
261
|
+
|
|
262
|
+
def __bool__(self) -> bool:
|
|
263
|
+
return bool(self.instance.descriptor.submodels)
|
|
264
|
+
|
|
265
|
+
def __len__(self) -> int:
|
|
266
|
+
return len(self.instance.descriptor.submodels)
|
|
267
|
+
|
|
268
|
+
def __getitem__(self, key:str) -> SubmodelService:
|
|
269
|
+
if isinstance(key, str):
|
|
270
|
+
for sm_desc in self.instance.descriptor.submodels:
|
|
271
|
+
if sm_desc.idShort == key:
|
|
272
|
+
return self.createSubmodelService(sm_desc)
|
|
273
|
+
raise ResourceNotFoundError.create("Submodel", f'idShort={key}')
|
|
274
|
+
else:
|
|
275
|
+
raise ValueError(f'Invalid Submodel key: {key}')
|
|
276
|
+
|
|
277
|
+
def __setitem__(self, key:str, value:SubmodelService) -> None:
|
|
278
|
+
raise NotImplementedError('SubmodelServiceCollection does not support set operation')
|
|
279
|
+
|
|
280
|
+
def __delitem__(self, key:str) -> None:
|
|
281
|
+
raise NotImplementedError('SubmodelServiceCollection does not support delete operation')
|
|
282
|
+
|
|
283
|
+
def find(self, **kwargs) -> Generator[SubmodelService, None, None]:
|
|
284
|
+
matches:list[InstanceSubmodelDescriptor] = self.instance.descriptor.submodels
|
|
285
|
+
for key, value in kwargs.items():
|
|
286
|
+
match key:
|
|
287
|
+
case 'idShort':
|
|
288
|
+
matches = [sm_desc for sm_desc in matches if sm_desc.idShort == value]
|
|
289
|
+
case 'semanticId':
|
|
290
|
+
matches = [sm_desc for sm_desc in matches if sm_desc.semanticId == value]
|
|
291
|
+
return (self.createSubmodelService(sm_desc) for sm_desc in matches)
|
|
292
|
+
|
|
293
|
+
def __repr__(self):
|
|
294
|
+
return 'SubmodelServiceCollection(instance={self.instance.descriptor.id})'
|
|
295
|
+
|
|
296
|
+
def createSubmodelService(self, sm_desc:InstanceSubmodelDescriptor) -> SubmodelService:
|
|
297
|
+
if not self.instance.descriptor.baseEndpoint:
|
|
298
|
+
raise ValueError(f'MDTInstance is not ready: id={self.instance.descriptor.id}, state={self.instance.descriptor.status}')
|
|
299
|
+
|
|
300
|
+
submodel_url = f'{self.instance.descriptor.baseEndpoint}/submodels/{to_base64_string(sm_desc.id)}'
|
|
301
|
+
if sm_desc:
|
|
302
|
+
if sm_desc.semanticId == MDT_SEMANTIC_ID.DATA:
|
|
303
|
+
if self.instance.descriptor.assetType == 'Machine':
|
|
304
|
+
return DataSubmodelServiceClient(self.instance.descriptor.id, sm_desc, submodel_url, 'Equipment')
|
|
305
|
+
elif self.instance.descriptor.assetType == 'Process':
|
|
306
|
+
return DataSubmodelServiceClient(submodel_url, sm_desc, 'Operation')
|
|
307
|
+
else:
|
|
308
|
+
return DataSubmodelServiceClient(self.instance.descriptor.id, sm_desc, submodel_url, 'Equipment')
|
|
309
|
+
elif sm_desc.semanticId == MDT_SEMANTIC_ID.AI:
|
|
310
|
+
return AIServiceClient(self.instance.descriptor.id, sm_desc, submodel_url)
|
|
311
|
+
elif sm_desc.semanticId == MDT_SEMANTIC_ID.SIMULATION:
|
|
312
|
+
return SimulationServiceClient(self.instance.descriptor.id, sm_desc, submodel_url)
|
|
313
|
+
elif sm_desc.semanticId == MDT_SEMANTIC_ID.INFORMATION_MODEL:
|
|
314
|
+
return InformationModelServiceClient(self.instance.descriptor.id, sm_desc, submodel_url)
|
|
315
|
+
else:
|
|
316
|
+
return HttpSubmodelServiceClient(self.instance.descriptor.id, sm_desc, submodel_url)
|