mdtpy 0.1.0__tar.gz

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-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdtpy
3
+ Version: 0.1.0
4
+ Summary: Python binding for MDT Platform
5
+ Author: Kang-Woo Lee
6
+ Author-email: kwlee@etri.re.kr
7
+ Requires-Python: >=3.11
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: basyx-python-sdk (>=2.0.0,<3.0.0)
14
+ Requires-Dist: dataclass-wizard (==0.22)
15
+ Requires-Dist: isodate (>=0.7.2,<0.8.0)
16
+ Requires-Dist: pandas (>=2.3.3,<3.0.0)
17
+ Requires-Dist: requests (>=2.32.5,<3.0.0)
18
+ Requires-Dist: requests-toolbelt (>=1.0.0,<2.0.0)
19
+ Requires-Dist: tika (>=3.1.0,<4.0.0)
20
+ Requires-Dist: urllib3 (>=2.6.2,<3.0.0)
21
+ Description-Content-Type: text/markdown
22
+
23
+
mdtpy-0.1.0/README.md ADDED
File without changes
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "mdtpy"
3
+ version = "0.1.0"
4
+ description = "Python binding for MDT Platform"
5
+ authors = [
6
+ {name = "Kang-Woo Lee",email = "kwlee@etri.re.kr"}
7
+ ]
8
+ readme = "README.md"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "basyx-python-sdk (>=2.0.0,<3.0.0)",
12
+ "urllib3 (>=2.6.2,<3.0.0)",
13
+ "requests (>=2.32.5,<3.0.0)",
14
+ "requests-toolbelt (>=1.0.0,<2.0.0)",
15
+ "dataclass-wizard (==0.22)",
16
+ "isodate (>=0.7.2,<0.8.0)",
17
+ "pandas (>=2.3.3,<3.0.0)",
18
+ "tika (>=3.1.0,<4.0.0)"
19
+ ]
20
+
21
+ [tool.poetry]
22
+ packages = [{include = "mdtpy", from = "src"}]
23
+
24
+ [[tool.poetry.source]]
25
+ name = "foo"
26
+ url = "https://pypi.example.org/simple/"
27
+ priority = "supplemental"
28
+
29
+
30
+ [[tool.poetry.source]]
31
+ name = "test"
32
+ url = "https://pypi.example.org/simple/"
33
+ priority = "supplemental"
34
+
35
+ [build-system]
36
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
37
+ build-backend = "poetry.core.masonry.api"
@@ -0,0 +1,12 @@
1
+
2
+ # SSL 인증서 검증 경고 억제
3
+ import urllib3
4
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
5
+
6
+ from .instance import *
7
+ from .value import *
8
+ from .descriptor import *
9
+ from .exceptions import *
10
+ from .timeseries import *
11
+ from . import aas_misc as aas
12
+ from . import basyx
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, cast, Optional, Iterable
4
+ from enum import Enum, auto
5
+
6
+ import json
7
+ import datetime
8
+ from dataclasses import dataclass, field
9
+ from dataclass_wizard import JSONWizard
10
+
11
+ from basyx.aas import model
12
+ from .basyx import serde as basyx_serde
13
+ from . import utils
14
+
15
+
16
+ class SecurityTypeEnum(Enum):
17
+ NONE = auto()
18
+ RFC_TLSA = auto()
19
+ W3C_DID = auto()
20
+
21
+ @dataclass(slots=True)
22
+ class SecurityAttributeObject(JSONWizard):
23
+ type: SecurityTypeEnum
24
+ key: str
25
+ value: str
26
+
27
+ @dataclass(slots=True)
28
+ class ProtocolInformation:
29
+ href: str|None
30
+ endpointProtocol: str|None = field(default=None)
31
+ endpointProtocolVersion: str|None = field(default=None)
32
+ subprotocol: str|None = field(default=None)
33
+ subprotocolBody: str|None = field(default=None)
34
+ subprotocolBody_encoding: str|None = field(default=None)
35
+ securityAttributes: list[SecurityAttributeObject] = field(default_factory=list)
36
+
37
+ @dataclass(slots=True)
38
+ class Endpoint:
39
+ interface: str
40
+ protocolInformation: ProtocolInformation
41
+
42
+
43
+ @dataclass(slots=True)
44
+ class OperationVariable:
45
+ value: model.SubmodelElement
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: dict) -> OperationVariable:
49
+ return cls(value=basyx_serde.from_dict(data['value']))
50
+
51
+ def to_dict(self) -> dict[str, Any]:
52
+ return { 'value': json.loads(basyx_serde.to_json(self.value)) }
53
+
54
+ @dataclass(slots=True)
55
+ class OperationResult:
56
+ messages: Optional[list[str]]
57
+ execution_state: str
58
+ success: bool
59
+ output_arguments: Optional[list[OperationVariable]]
60
+ inoutput_arguments: Optional[list[OperationVariable]]
61
+
62
+ @classmethod
63
+ def from_dict(cls, data: dict) -> OperationResult:
64
+ output_arguments = data.get('outputArguments')
65
+ if output_arguments:
66
+ output_arguments = [OperationVariable.from_dict(arg) for arg in output_arguments]
67
+ inoutput_arguments = data.get('inoutputArguments')
68
+ if inoutput_arguments:
69
+ inoutput_arguments = [OperationVariable.from_dict(arg) for arg in inoutput_arguments]
70
+
71
+ return cls(messages = data.get('messages'),
72
+ execution_state = data['executionState'],
73
+ success = data['success'],
74
+ output_arguments=output_arguments,
75
+ inoutput_arguments=inoutput_arguments)
76
+
77
+ @classmethod
78
+ def from_json(cls, json_str: str) -> OperationResult:
79
+ return cls.from_dict(json.loads(json_str))
80
+
81
+ @dataclass(slots=True)
82
+ class OperationHandle(JSONWizard):
83
+ handle_id: str
84
+
85
+ @dataclass(slots=True)
86
+ class OperationRequest:
87
+ input_arguments: Iterable[OperationVariable]
88
+ inoutput_arguments: Iterable[OperationVariable]
89
+ client_timeout_duration: datetime.timedelta
90
+
91
+ def to_json(self) -> str:
92
+ in_opv_list = [ op_var.to_dict() for op_var in self.input_arguments ]
93
+ inout_opv_list = [ op_var.to_dict() for op_var in self.inoutput_arguments ]
94
+ return json.dumps({
95
+ 'inputArguments': in_opv_list,
96
+ # 'inoutputArguments': inout_opv_list,
97
+ 'clientTimeoutDuration': utils.timedelta_to_iso8601(self.client_timeout_duration)
98
+ })
99
+
100
+ from .value import MultiLanguagePropertyValue, get_value
101
+ def get_first_text(mlpv:MultiLanguagePropertyValue|model.MultiLanguageProperty) -> str|None:
102
+ if isinstance(mlpv, model.MultiLanguageProperty):
103
+ mlpv = cast(MultiLanguagePropertyValue, get_value(mlpv))
104
+ return next(iter(mlpv.values())) if mlpv else None
@@ -0,0 +1,2 @@
1
+ from . import serde
2
+ from . import utils
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+ import json
5
+
6
+ from basyx.aas.adapter.json.json_deserialization import AASFromJsonDecoder
7
+ from basyx.aas.adapter.json.json_serialization import AASToJsonEncoder
8
+
9
+
10
+ def from_json(json_str:str) -> Any:
11
+ return json.loads(json_str, cls=AASFromJsonDecoder)
12
+
13
+ def from_dict(data:dict) -> Any:
14
+ json_str = json.dumps(data)
15
+ return json.loads(json_str, cls=AASFromJsonDecoder)
16
+
17
+ def to_json(obj:Any) -> str:
18
+ return json.dumps(obj, cls=AASToJsonEncoder)
@@ -0,0 +1,169 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+ import re
5
+
6
+ from dateutil.relativedelta import relativedelta
7
+
8
+
9
+ _ISO8601_DURATION_RE = re.compile(
10
+ r"""
11
+ ^\s*
12
+ (?P<sign>[+-])?
13
+ P
14
+ (?:
15
+ (?:(?P<years>\d+(?:[.,]\d+)?)Y)?
16
+ (?:(?P<months>\d+(?:[.,]\d+)?)M)?
17
+ (?:(?P<weeks>\d+(?:[.,]\d+)?)W)?
18
+ (?:(?P<days>\d+(?:[.,]\d+)?)D)?
19
+ )?
20
+ (?:T
21
+ (?:(?P<hours>\d+(?:[.,]\d+)?)H)?
22
+ (?:(?P<minutes>\d+(?:[.,]\d+)?)M)?
23
+ (?:(?P<seconds>\d+(?:[.,]\d+)?)S)?
24
+ )?
25
+ \s*$
26
+ """,
27
+ re.VERBOSE | re.IGNORECASE,
28
+ )
29
+
30
+
31
+ def _to_number(s: Optional[str]) -> float:
32
+ if not s:
33
+ return 0.0
34
+ return float(s.replace(",", "."))
35
+
36
+
37
+ def parse_iso8601_to_relativedelta(duration: str) -> relativedelta:
38
+ """
39
+ Parse an ISO 8601 duration string and return a dateutil.relativedelta.relativedelta.
40
+
41
+ Examples:
42
+ - "P1Y2M3DT4H5M6S"
43
+ - "PT15M"
44
+ - "P2W"
45
+ - "-P3DT12H"
46
+ - "PT0.5S"
47
+ """
48
+ m = _ISO8601_DURATION_RE.match(duration)
49
+ if not m:
50
+ raise ValueError(f"Invalid ISO 8601 duration: {duration!r}")
51
+
52
+ sign = -1 if (m.group("sign") == "-") else 1
53
+
54
+ years = _to_number(m.group("years"))
55
+ months = _to_number(m.group("months"))
56
+ weeks = _to_number(m.group("weeks"))
57
+ days = _to_number(m.group("days"))
58
+ hours = _to_number(m.group("hours"))
59
+ minutes = _to_number(m.group("minutes"))
60
+ seconds = _to_number(m.group("seconds"))
61
+
62
+ # ISO 8601 allows fractional values; relativedelta expects integers for Y/M
63
+ # (fractional months/years are ambiguous). We reject fractional Y/M explicitly.
64
+ if years and not years.is_integer():
65
+ raise ValueError(f"Fractional years not supported for relativedelta: {duration!r}")
66
+ if months and not months.is_integer():
67
+ raise ValueError(f"Fractional months not supported for relativedelta: {duration!r}")
68
+
69
+ # For weeks/days/hours/minutes/seconds, we can carry fractions down to smaller units.
70
+ # Convert weeks to days.
71
+ total_days = days + (weeks * 7.0)
72
+
73
+ # Split fractional days into seconds
74
+ day_int = int(total_days)
75
+ day_frac = total_days - day_int
76
+
77
+ total_seconds = seconds + (minutes * 60.0) + (hours * 3600.0) + (day_frac * 86400.0)
78
+
79
+ sec_int = int(total_seconds)
80
+ sec_frac = total_seconds - sec_int
81
+ microseconds = int(round(sec_frac * 1_000_000))
82
+
83
+ # Handle rounding overflow (e.g., 0.9999996 rounds to 1_000_000)
84
+ if microseconds == 1_000_000:
85
+ sec_int += 1
86
+ microseconds = 0
87
+
88
+ rd = relativedelta(
89
+ years=sign * int(years),
90
+ months=sign * int(months),
91
+ days=sign * day_int,
92
+ seconds=sign * sec_int,
93
+ microseconds=sign * microseconds,
94
+ )
95
+ return rd
96
+
97
+
98
+ def relativedelta_to_iso8601(rd: relativedelta) -> str:
99
+ """
100
+ Convert dateutil.relativedelta.relativedelta to an ISO 8601 duration string.
101
+
102
+ Examples:
103
+ relativedelta(years=1, months=2, days=3,
104
+ hours=4, minutes=5, seconds=6)
105
+ -> "P1Y2M3DT4H5M6S"
106
+ """
107
+
108
+ # Determine sign (ISO-8601 uses a single leading sign)
109
+ def _sign(x: int) -> int:
110
+ return -1 if x < 0 else 1
111
+
112
+ signs = [
113
+ _sign(rd.years),
114
+ _sign(rd.months),
115
+ _sign(rd.days),
116
+ _sign(rd.hours),
117
+ _sign(rd.minutes),
118
+ _sign(rd.seconds),
119
+ _sign(rd.microseconds),
120
+ ]
121
+ sign = "-" if any(s < 0 for s in signs) else ""
122
+
123
+ # Use absolute values (ISO-8601 sign is global)
124
+ years = abs(rd.years)
125
+ months = abs(rd.months)
126
+ days = abs(rd.days)
127
+
128
+ hours = abs(rd.hours)
129
+ minutes = abs(rd.minutes)
130
+
131
+ seconds = abs(rd.seconds)
132
+ microseconds = abs(rd.microseconds)
133
+
134
+ # Combine seconds + microseconds
135
+ if microseconds:
136
+ seconds = seconds + microseconds / 1_000_000
137
+
138
+ date_parts = []
139
+ time_parts = []
140
+
141
+ if years:
142
+ date_parts.append(f"{years}Y")
143
+ if months:
144
+ date_parts.append(f"{months}M")
145
+ if days:
146
+ date_parts.append(f"{days}D")
147
+
148
+ if hours:
149
+ time_parts.append(f"{hours}H")
150
+ if minutes:
151
+ time_parts.append(f"{minutes}M")
152
+ if seconds:
153
+ # Strip trailing .0 for integers
154
+ if float(seconds).is_integer():
155
+ time_parts.append(f"{int(seconds)}S")
156
+ else:
157
+ time_parts.append(f"{seconds:.6f}".rstrip("0").rstrip(".") + "S")
158
+
159
+ # ISO-8601 requires at least one component
160
+ if not date_parts and not time_parts:
161
+ return "PT0S"
162
+
163
+ result = sign + "P"
164
+ result += "".join(date_parts)
165
+
166
+ if time_parts:
167
+ result += "T" + "".join(time_parts)
168
+
169
+ return result
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+ from dataclasses import dataclass, field
5
+ from enum import Enum
6
+
7
+ from dataclass_wizard import JSONWizard
8
+
9
+
10
+ class MDTInstanceStatus(Enum):
11
+ STOPPED = "STOPPED"
12
+ STARTING = "STARTING"
13
+ RUNNING = "RUNNING"
14
+ STOPPING = "STOPPING"
15
+ FAILED = "FAILED"
16
+
17
+ class MDTAssetType(Enum):
18
+ Machine = "Machine"
19
+ Process = "Process"
20
+ Line = "Line"
21
+ Factory = "Factory"
22
+
23
+ class AssetKind(Enum):
24
+ INSTANCE = "INSTANCE"
25
+ NOT_APPLICABLE = "NOT_APPLICABLE"
26
+ TYPE = "TYPE"
27
+
28
+
29
+ @dataclass(frozen=True, slots=True)
30
+ class InstanceDescriptor(JSONWizard):
31
+ """
32
+ MDTInstance Descriptor
33
+
34
+ Attributes:
35
+ ----------
36
+ id: str
37
+ The unique identifier of the MDTInstance.
38
+ status: MDTInstanceStatus
39
+ The Status of the MDTInstance.
40
+ base_endpoint: Optional[str]
41
+ The Base Endpoint of the MDTInstance.
42
+ aas_id: str
43
+ The AAS ID of the MDTInstance.
44
+ aas_idshort: Optional[str]
45
+ The AAS ID Short of the MDTInstance.
46
+ global_asset_id: Optional[str]
47
+ The Global Asset ID of the MDTInstance.
48
+ asset_type: Optional[MDTAssetType]
49
+ The Asset Type of the MDTInstance.
50
+ asset_kind: Optional[AssetKind]
51
+ The Asset Kind of the MDTInstance.
52
+ """
53
+ id: str
54
+ status: MDTInstanceStatus
55
+ aas_id: str
56
+ base_endpoint: Optional[str] = field(default=None, hash=False, compare=False)
57
+ aas_id_short: Optional[str] = field(default=None, hash=False, compare=False)
58
+ global_asset_id: Optional[str] = field(default=None, hash=False, compare=False)
59
+ asset_type: MDTAssetType|None = field(default=None, hash=False, compare=False)
60
+ asset_kind: AssetKind|None = field(default=None, hash=False, compare=False)
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class MDTParameterDescriptor(JSONWizard):
64
+ id: str
65
+ value_type: str
66
+ reference: str
67
+ name: Optional[str] = None
68
+ endpoint: Optional[str] = None
69
+
70
+
71
+ SEMANTIC_ID_INFOR_MODEL_SUBMODEL = "https://etri.re.kr/mdt/Submodel/InformationModel/1/1"
72
+ SEMANTIC_ID_AI_SUBMODEL = "https://etri.re.kr/mdt/Submodel/AI/1/1"
73
+ SEMANTIC_ID_SIMULATION_SUBMODEL = "https://etri.re.kr/mdt/Submodel/Simulation/1/1"
74
+ SEMANTIC_ID_DATA_SUBMODEL = "https://etri.re.kr/mdt/Submodel/Data/1/1"
75
+ SEMANTIC_ID_TIME_SERIES_SUBMODEL = 'https://admin-shell.io/idta/TimeSeries/1/1'
76
+
77
+ @dataclass(frozen=True, slots=True)
78
+ class MDTSubmodelDescriptor(JSONWizard):
79
+ id: str
80
+ id_short: str
81
+ semantic_id: str
82
+ endpoint: Optional[str]
83
+
84
+ def is_information_model(self) -> bool:
85
+ return self.semantic_id == SEMANTIC_ID_INFOR_MODEL_SUBMODEL
86
+
87
+ def is_data(self) -> bool:
88
+ return self.semantic_id == SEMANTIC_ID_DATA_SUBMODEL
89
+
90
+ def is_simulation(self) -> bool:
91
+ return self.semantic_id == SEMANTIC_ID_SIMULATION_SUBMODEL
92
+
93
+ def is_ai(self) -> bool:
94
+ return self.semantic_id == SEMANTIC_ID_AI_SUBMODEL
95
+
96
+ def is_time_series(self) -> bool:
97
+ return self.semantic_id == SEMANTIC_ID_TIME_SERIES_SUBMODEL
98
+
99
+
100
+ @dataclass(frozen=True, slots=True)
101
+ class ArgumentDescriptor(JSONWizard):
102
+ id: str
103
+ id_short_path: str
104
+ value_type: str
105
+ reference: str
106
+
107
+
108
+ @dataclass(frozen=True, slots=True)
109
+ class MDTOperationDescriptor(JSONWizard):
110
+ id: str
111
+ operation_type: str
112
+ input_arguments: list[ArgumentDescriptor]
113
+ output_arguments: list[ArgumentDescriptor]
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class MDTException(Exception):
5
+ def __init__(self, details:str) -> None:
6
+ self.details = details
7
+ super().__init__(details)
8
+
9
+ def __str__(self) -> str:
10
+ return repr(self)
11
+
12
+
13
+ class InternalError(MDTException):
14
+ def __init__(self, details:str) -> None:
15
+ super().__init__(details)
16
+
17
+
18
+ class TimeoutError(MDTException):
19
+ def __init__(self, details:str) -> None:
20
+ super().__init__(details)
21
+
22
+
23
+ class CancellationError(MDTException):
24
+ def __init__(self, details:str) -> None:
25
+ super().__init__(details)
26
+
27
+
28
+ class OperationError(MDTException):
29
+ def __init__(self, details:str) -> None:
30
+ super().__init__(details)
31
+
32
+
33
+ class RemoteError(MDTException):
34
+ def __init__(self, details:str) -> None:
35
+ super().__init__(details)
36
+
37
+ import requests
38
+ class MDTInstanceConnectionError(MDTException):
39
+ def __init__(self, details:str, cause:requests.exceptions.ConnectionError) -> None:
40
+ super().__init__(details)
41
+ self.cause = cause
42
+
43
+ def __repr__(self) -> str:
44
+ return f"{self.__class__.__name__}(details={self.details}, cause={self.cause})"
45
+
46
+
47
+ class ResourceAlreadyExistsError(MDTException):
48
+ def __init__(self, details:str) -> None:
49
+ super().__init__(details)
50
+
51
+ @classmethod
52
+ def create(cls, resource_type:str, id_spec:str):
53
+ return ResourceAlreadyExistsError(f"Resource(type={resource_type}, {id_spec})")
54
+
55
+
56
+ class ResourceNotFoundError(MDTException):
57
+ def __init__(self, details:str) -> None:
58
+ super().__init__(details)
59
+
60
+ @classmethod
61
+ def create(cls, resource_type:str, id_spec:str):
62
+ return ResourceNotFoundError(f"Resource(type={resource_type}, {id_spec})")
63
+
64
+
65
+ class InvalidResourceStateError(MDTException):
66
+ def __init__(self, details:str) -> None:
67
+ super().__init__(details)
68
+
69
+ @classmethod
70
+ def create(cls, resource_type:str, id_spec:str, status):
71
+ return InvalidResourceStateError(f"Resource(type={resource_type}, {id_spec}), status={status}")