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 ADDED
@@ -0,0 +1,4 @@
1
+ from .model import MDTException, InternalError, ResourceNotFoundError
2
+ from mdtpy.client import connect, HttpMDTManagerClient
3
+
4
+ __version__ = '25.02.03'
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
@@ -0,0 +1,7 @@
1
+
2
+ from .http_client import *
3
+ from .http_service_client import *
4
+ from .http_instance_client import *
5
+ from .http_registry_client import *
6
+ from .http_repository_client import *
7
+ from .http_fa3st_client import *
@@ -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)