mdtpy 0.2.2__tar.gz → 0.2.4__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.
Files changed (64) hide show
  1. mdtpy-0.2.4/PKG-INFO +139 -0
  2. mdtpy-0.2.4/README.md +116 -0
  3. mdtpy-0.2.4/pyproject.toml +49 -0
  4. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/__init__.py +1 -1
  5. mdtpy-0.2.4/src/mdtpy/aas_misc.py +220 -0
  6. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/airflow/invocation.py +4 -19
  7. mdtpy-0.2.4/src/mdtpy/descriptor.py +165 -0
  8. mdtpy-0.2.4/src/mdtpy/exceptions.py +176 -0
  9. mdtpy-0.2.4/src/mdtpy/fa3st.py +307 -0
  10. mdtpy-0.2.4/src/mdtpy/http_client.py +177 -0
  11. mdtpy-0.2.4/src/mdtpy/instance.py +704 -0
  12. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/operation.py +38 -18
  13. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/parameter.py +24 -18
  14. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/reference.py +5 -10
  15. mdtpy-0.2.4/src/mdtpy/submodel.py +491 -0
  16. mdtpy-0.2.4/src/mdtpy/timeseries.py +371 -0
  17. mdtpy-0.2.4/src/mdtpy/utils.py +253 -0
  18. mdtpy-0.2.4/src/mdtpy/value.py +364 -0
  19. mdtpy-0.2.4/src/mdtpy.egg-info/PKG-INFO +139 -0
  20. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy.egg-info/SOURCES.txt +21 -1
  21. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy.egg-info/requires.txt +1 -3
  22. mdtpy-0.2.4/src/samples/sample_add_and_sleep.py +16 -0
  23. mdtpy-0.2.4/src/samples/sample_inspector_simiulation.py +113 -0
  24. mdtpy-0.2.4/src/samples/sample_instance.py +40 -0
  25. mdtpy-0.2.4/src/samples/sample_multi_ops.py +48 -0
  26. mdtpy-0.2.4/src/samples/sample_operations.py +23 -0
  27. mdtpy-0.2.4/src/samples/sample_parameters.py +29 -0
  28. mdtpy-0.2.4/src/samples/sample_reference.py +37 -0
  29. mdtpy-0.2.4/src/samples/sample_submodel_elements.py +12 -0
  30. mdtpy-0.2.4/src/samples/sample_timeseries.py +25 -0
  31. mdtpy-0.2.4/tests/test_aas_misc.py +254 -0
  32. mdtpy-0.2.4/tests/test_descriptor.py +338 -0
  33. mdtpy-0.2.4/tests/test_fa3st.py +389 -0
  34. mdtpy-0.2.4/tests/test_http_client.py +237 -0
  35. mdtpy-0.2.4/tests/test_instance.py +604 -0
  36. mdtpy-0.2.4/tests/test_operation.py +444 -0
  37. mdtpy-0.2.4/tests/test_parameter.py +168 -0
  38. mdtpy-0.2.4/tests/test_submodel.py +511 -0
  39. mdtpy-0.2.4/tests/test_timeseries.py +403 -0
  40. mdtpy-0.2.4/tests/test_utils.py +301 -0
  41. mdtpy-0.2.4/tests/test_value.py +370 -0
  42. mdtpy-0.2.2/PKG-INFO +0 -16
  43. mdtpy-0.2.2/README.md +0 -0
  44. mdtpy-0.2.2/pyproject.toml +0 -25
  45. mdtpy-0.2.2/src/mdtpy/aas_misc.py +0 -102
  46. mdtpy-0.2.2/src/mdtpy/descriptor.py +0 -171
  47. mdtpy-0.2.2/src/mdtpy/exceptions.py +0 -83
  48. mdtpy-0.2.2/src/mdtpy/fa3st.py +0 -169
  49. mdtpy-0.2.2/src/mdtpy/http_client.py +0 -66
  50. mdtpy-0.2.2/src/mdtpy/instance.py +0 -610
  51. mdtpy-0.2.2/src/mdtpy/submodel.py +0 -409
  52. mdtpy-0.2.2/src/mdtpy/timeseries.py +0 -269
  53. mdtpy-0.2.2/src/mdtpy/utils.py +0 -120
  54. mdtpy-0.2.2/src/mdtpy/value.py +0 -228
  55. mdtpy-0.2.2/src/mdtpy.egg-info/PKG-INFO +0 -16
  56. {mdtpy-0.2.2 → mdtpy-0.2.4}/setup.cfg +0 -0
  57. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/airflow/__init__.py +0 -0
  58. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/airflow/argument_spec.py +0 -0
  59. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/airflow/dag_context.py +0 -0
  60. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/basyx/__init__.py +0 -0
  61. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/basyx/serde.py +0 -0
  62. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/basyx/utils.py +0 -0
  63. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy.egg-info/dependency_links.txt +0 -0
  64. {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy.egg-info/top_level.txt +0 -0
mdtpy-0.2.4/PKG-INFO ADDED
@@ -0,0 +1,139 @@
1
+ Metadata-Version: 2.4
2
+ Name: mdtpy
3
+ Version: 0.2.4
4
+ Summary: A Python client library for the MDT (Manufacturing Digital Twin) Platform.
5
+ Author-email: Kang-Woo Lee <kwlee@etri.re.kr>
6
+ Keywords: MDT,AAS,digital-twin,manufacturing,basyx
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Scientific/Engineering
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ Requires-Dist: basyx-python-sdk<3.0.0,>=2.0.0
16
+ Requires-Dist: dataclass-wizard<1,>=0.22
17
+ Requires-Dist: isodate>=0.7.2
18
+ Requires-Dist: jinja2>=3.1.6
19
+ Requires-Dist: pandas>=2.3.3
20
+ Requires-Dist: requests>=2.32.5
21
+ Requires-Dist: typing_extensions>=4.2.0
22
+ Requires-Dist: urllib3<3.0.0,>=2.6.2
23
+
24
+ # mdtpy
25
+
26
+ MDT(Manufacturing Digital Twin) 플랫폼을 위한 Python 클라이언트 라이브러리.
27
+ Asset Administration Shell(AAS) 표준을 기반으로 MDT Instance Manager 및 개별
28
+ FA³ST 인스턴스에 HTTP REST로 접근하는 API를 제공한다.
29
+
30
+ 내부적으로 [`basyx-python-sdk`](https://pypi.org/project/basyx-python-sdk/)를
31
+ 사용해 AAS 모델(`Property`, `SubmodelElementCollection`, `SubmodelElementList`,
32
+ `File`, `Range`, `MultiLanguageProperty`, `Operation`, `TimeSeries` 등)을 다룬다.
33
+
34
+ ## 요구 사항
35
+
36
+ - Python 3.10 이상
37
+ - [uv](https://docs.astral.sh/uv/) (의존성/빌드 관리)
38
+
39
+ ## 설치
40
+
41
+ ```bash
42
+ uv sync # 런타임 의존성
43
+ make install-dev # 개발 의존성(pytest)까지 함께 설치
44
+ ```
45
+
46
+ 소스 레이아웃은 `src/mdtpy/`이며, `src/`가 Python path에 등록되어 있다
47
+ (`.vscode/settings.json`).
48
+
49
+ ## 빠른 시작
50
+
51
+ ```python
52
+ import mdtpy
53
+
54
+ # 1. MDT Instance Manager에 접속
55
+ manager = mdtpy.connect("http://localhost:12985/instance-manager")
56
+
57
+ # 2. 인스턴스 가져오기 / 시작
58
+ instance = manager.instances['my_twin']
59
+ if not instance.is_running():
60
+ instance.start()
61
+
62
+ # 3. 파라미터 읽기/쓰기
63
+ param = instance.parameters['Status']
64
+ print(param.read_value())
65
+ param.update_value('Running')
66
+
67
+ # 4. Operation 호출
68
+ op = instance.operations['Inspect']
69
+ result = op.invoke(Image=instance.parameters['UpperImage'])
70
+ op.output_arguments.update_value(result)
71
+
72
+ # 5. 시계열 데이터
73
+ ts = instance.timeseries['WelderAmpereLog'].timeseries()
74
+ df = ts.segments['Latest'].records_as_pandas()
75
+ ```
76
+
77
+ 상세 사용법은 [`doc/programming_guide.md`](doc/programming_guide.md) 참조.
78
+
79
+ ## 주요 모듈
80
+
81
+ | 모듈 | 역할 |
82
+ |---|---|
83
+ | `mdtpy.instance` | `connect()`, `MDTInstanceManager`, `MDTInstance`, 컬렉션, 폴러 |
84
+ | `mdtpy.reference` | `ElementReference` 추상화 (`DefaultElementReference`, `LazyElementReference`) |
85
+ | `mdtpy.parameter` | `MDTParameter`, `MDTParameterCollection` |
86
+ | `mdtpy.submodel` | `SubmodelService`, `SubmodelServiceCollection`, `SubmodelElementCollection` |
87
+ | `mdtpy.operation` | `OperationSubmodelService`, `Argument`, `ArgumentList` |
88
+ | `mdtpy.timeseries` | `TimeSeriesService` (pandas 통합) |
89
+ | `mdtpy.value` | SME ↔ Python 값 ↔ 서버 wire JSON 변환 |
90
+ | `mdtpy.descriptor` | 불변 dataclass 디스크립터, semantic_id 기반 분류 |
91
+ | `mdtpy.aas_misc` | AAS wire 포맷 dataclass (`Endpoint`, `OperationVariable` 등) |
92
+ | `mdtpy.fa3st` | 개별 FA³ST 인스턴스용 HTTP 헬퍼 (`call_get`/`call_put`/...) |
93
+ | `mdtpy.http_client` | Instance Manager용 응답 파서, 공통 예외 변환 |
94
+ | `mdtpy.exceptions` | `MDTException` 계층 |
95
+ | `mdtpy.utils` | ISO 8601 / timedelta / SME→Python 변환 헬퍼 |
96
+ | `mdtpy.airflow` | Apache Airflow DAG 통합 (선택, 자동 import되지 않음) |
97
+ | `mdtpy.basyx.serde` | basyx-python-sdk 직렬화 래퍼 |
98
+
99
+ ## 개발
100
+
101
+ ### 테스트 실행
102
+
103
+ ```bash
104
+ make test # 전체 pytest suite
105
+ make test-cov # 커버리지 보고서 포함
106
+ ```
107
+
108
+ 또는 직접 실행:
109
+
110
+ ```bash
111
+ uv run --env-file .env pytest tests/test_instance.py
112
+ uv run --env-file .env pytest tests/test_instance.py::TestMDTInstanceCollection -v
113
+ ```
114
+
115
+ > **참고**: ROS2(`/opt/ros/humble/...`)를 source한 셸에서는 시스템
116
+ > `launch_pytest` 플러그인이 자동 로드되어 `yaml` 누락으로 충돌한다. `Makefile`
117
+ > 과 `.env`가 `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`을 주입하여 이를 우회한다.
118
+ > ROS가 source되지 않은 셸이라면 `uv run pytest`만으로도 충분하다.
119
+
120
+ `tests/` 폴더의 단위 테스트는 외부 서버 의존 없이 mock으로 동작한다
121
+ (300+ tests). `src/samples/sample_*.py` 스크립트들은 **실서버 대상 사용 예제 /
122
+ 스모크 테스트**이므로 별도 환경에서 실행한다 (예: `python src/samples/sample_reference.py`).
123
+
124
+ ### 코드 스타일
125
+
126
+ - 코드 주석/docstring: 한국어 (평서문 "~한다")
127
+ - 로깅/예외 메시지: 영어
128
+ - import 순서: `__future__` → `typing` → 표준 → 서드파티 → 로컬
129
+ - 타입 힌트: built-in 우선 (`list`/`dict`/`tuple`), `Optional[X]` 권장
130
+ - 들여쓰기: 4-space
131
+ - 라인 길이: 100자 (신규 코드)
132
+
133
+ 자세한 규칙과 아키텍처는 [`CLAUDE.md`](CLAUDE.md) 참조.
134
+
135
+ ## 빌드
136
+
137
+ ```bash
138
+ uv build # sdist + wheel을 dist/에 생성
139
+ ```
mdtpy-0.2.4/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # mdtpy
2
+
3
+ MDT(Manufacturing Digital Twin) 플랫폼을 위한 Python 클라이언트 라이브러리.
4
+ Asset Administration Shell(AAS) 표준을 기반으로 MDT Instance Manager 및 개별
5
+ FA³ST 인스턴스에 HTTP REST로 접근하는 API를 제공한다.
6
+
7
+ 내부적으로 [`basyx-python-sdk`](https://pypi.org/project/basyx-python-sdk/)를
8
+ 사용해 AAS 모델(`Property`, `SubmodelElementCollection`, `SubmodelElementList`,
9
+ `File`, `Range`, `MultiLanguageProperty`, `Operation`, `TimeSeries` 등)을 다룬다.
10
+
11
+ ## 요구 사항
12
+
13
+ - Python 3.10 이상
14
+ - [uv](https://docs.astral.sh/uv/) (의존성/빌드 관리)
15
+
16
+ ## 설치
17
+
18
+ ```bash
19
+ uv sync # 런타임 의존성
20
+ make install-dev # 개발 의존성(pytest)까지 함께 설치
21
+ ```
22
+
23
+ 소스 레이아웃은 `src/mdtpy/`이며, `src/`가 Python path에 등록되어 있다
24
+ (`.vscode/settings.json`).
25
+
26
+ ## 빠른 시작
27
+
28
+ ```python
29
+ import mdtpy
30
+
31
+ # 1. MDT Instance Manager에 접속
32
+ manager = mdtpy.connect("http://localhost:12985/instance-manager")
33
+
34
+ # 2. 인스턴스 가져오기 / 시작
35
+ instance = manager.instances['my_twin']
36
+ if not instance.is_running():
37
+ instance.start()
38
+
39
+ # 3. 파라미터 읽기/쓰기
40
+ param = instance.parameters['Status']
41
+ print(param.read_value())
42
+ param.update_value('Running')
43
+
44
+ # 4. Operation 호출
45
+ op = instance.operations['Inspect']
46
+ result = op.invoke(Image=instance.parameters['UpperImage'])
47
+ op.output_arguments.update_value(result)
48
+
49
+ # 5. 시계열 데이터
50
+ ts = instance.timeseries['WelderAmpereLog'].timeseries()
51
+ df = ts.segments['Latest'].records_as_pandas()
52
+ ```
53
+
54
+ 상세 사용법은 [`doc/programming_guide.md`](doc/programming_guide.md) 참조.
55
+
56
+ ## 주요 모듈
57
+
58
+ | 모듈 | 역할 |
59
+ |---|---|
60
+ | `mdtpy.instance` | `connect()`, `MDTInstanceManager`, `MDTInstance`, 컬렉션, 폴러 |
61
+ | `mdtpy.reference` | `ElementReference` 추상화 (`DefaultElementReference`, `LazyElementReference`) |
62
+ | `mdtpy.parameter` | `MDTParameter`, `MDTParameterCollection` |
63
+ | `mdtpy.submodel` | `SubmodelService`, `SubmodelServiceCollection`, `SubmodelElementCollection` |
64
+ | `mdtpy.operation` | `OperationSubmodelService`, `Argument`, `ArgumentList` |
65
+ | `mdtpy.timeseries` | `TimeSeriesService` (pandas 통합) |
66
+ | `mdtpy.value` | SME ↔ Python 값 ↔ 서버 wire JSON 변환 |
67
+ | `mdtpy.descriptor` | 불변 dataclass 디스크립터, semantic_id 기반 분류 |
68
+ | `mdtpy.aas_misc` | AAS wire 포맷 dataclass (`Endpoint`, `OperationVariable` 등) |
69
+ | `mdtpy.fa3st` | 개별 FA³ST 인스턴스용 HTTP 헬퍼 (`call_get`/`call_put`/...) |
70
+ | `mdtpy.http_client` | Instance Manager용 응답 파서, 공통 예외 변환 |
71
+ | `mdtpy.exceptions` | `MDTException` 계층 |
72
+ | `mdtpy.utils` | ISO 8601 / timedelta / SME→Python 변환 헬퍼 |
73
+ | `mdtpy.airflow` | Apache Airflow DAG 통합 (선택, 자동 import되지 않음) |
74
+ | `mdtpy.basyx.serde` | basyx-python-sdk 직렬화 래퍼 |
75
+
76
+ ## 개발
77
+
78
+ ### 테스트 실행
79
+
80
+ ```bash
81
+ make test # 전체 pytest suite
82
+ make test-cov # 커버리지 보고서 포함
83
+ ```
84
+
85
+ 또는 직접 실행:
86
+
87
+ ```bash
88
+ uv run --env-file .env pytest tests/test_instance.py
89
+ uv run --env-file .env pytest tests/test_instance.py::TestMDTInstanceCollection -v
90
+ ```
91
+
92
+ > **참고**: ROS2(`/opt/ros/humble/...`)를 source한 셸에서는 시스템
93
+ > `launch_pytest` 플러그인이 자동 로드되어 `yaml` 누락으로 충돌한다. `Makefile`
94
+ > 과 `.env`가 `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`을 주입하여 이를 우회한다.
95
+ > ROS가 source되지 않은 셸이라면 `uv run pytest`만으로도 충분하다.
96
+
97
+ `tests/` 폴더의 단위 테스트는 외부 서버 의존 없이 mock으로 동작한다
98
+ (300+ tests). `src/samples/sample_*.py` 스크립트들은 **실서버 대상 사용 예제 /
99
+ 스모크 테스트**이므로 별도 환경에서 실행한다 (예: `python src/samples/sample_reference.py`).
100
+
101
+ ### 코드 스타일
102
+
103
+ - 코드 주석/docstring: 한국어 (평서문 "~한다")
104
+ - 로깅/예외 메시지: 영어
105
+ - import 순서: `__future__` → `typing` → 표준 → 서드파티 → 로컬
106
+ - 타입 힌트: built-in 우선 (`list`/`dict`/`tuple`), `Optional[X]` 권장
107
+ - 들여쓰기: 4-space
108
+ - 라인 길이: 100자 (신규 코드)
109
+
110
+ 자세한 규칙과 아키텍처는 [`CLAUDE.md`](CLAUDE.md) 참조.
111
+
112
+ ## 빌드
113
+
114
+ ```bash
115
+ uv build # sdist + wheel을 dist/에 생성
116
+ ```
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "mdtpy"
7
+ version = "0.2.4"
8
+ description = "A Python client library for the MDT (Manufacturing Digital Twin) Platform."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ authors = [{ name = "Kang-Woo Lee", email = "kwlee@etri.re.kr" }]
12
+ keywords = ["MDT", "AAS", "digital-twin", "manufacturing", "basyx"]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Scientific/Engineering",
20
+ ]
21
+ dependencies = [
22
+ "basyx-python-sdk>=2.0.0,<3.0.0",
23
+ "dataclass-wizard>=0.22,<1",
24
+ "isodate>=0.7.2",
25
+ "jinja2>=3.1.6",
26
+ "pandas>=2.3.3",
27
+ "requests>=2.32.5",
28
+ "typing_extensions>=4.2.0",
29
+ "urllib3>=2.6.2,<3.0.0",
30
+ ]
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+ include = ["mdtpy*"]
35
+ exclude = ["samples*"]
36
+
37
+ [dependency-groups]
38
+ dev = [
39
+ "pytest>=8.0",
40
+ ]
41
+
42
+ [tool.pytest.ini_options]
43
+ testpaths = ["tests"]
44
+ # 참고: ROS2를 source한 쉘에서는 /opt/ros/.../launch_pytest 플러그인이
45
+ # 자동 로드되어 yaml 누락으로 충돌한다. `.env`의 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
46
+ # 을 uv가 픽업하도록 호출해야 하므로 다음 중 하나를 사용한다.
47
+ # make test
48
+ # uv run --env-file .env pytest
49
+ # 또는 셸에 `export UV_ENV_FILE=.env`를 등록하면 `uv run pytest`도 그대로 동작한다.
@@ -1,4 +1,4 @@
1
- __version__ = "0.2.2"
1
+ __version__ = "0.2.4"
2
2
 
3
3
  # SSL 인증서 검증 경고 억제
4
4
  import urllib3
@@ -0,0 +1,220 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, 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
+ """AAS Endpoint의 보안 속성 종류."""
18
+
19
+ NONE = auto()
20
+ RFC_TLSA = auto()
21
+ W3C_DID = auto()
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class SecurityAttributeObject(JSONWizard):
26
+ """
27
+ AAS Endpoint에 부여되는 보안 속성 단위.
28
+
29
+ Attributes:
30
+ type (SecurityTypeEnum): 보안 속성 종류.
31
+ key (str): 속성 키.
32
+ value (str): 속성 값.
33
+ """
34
+ type: SecurityTypeEnum
35
+ key: str
36
+ value: str
37
+
38
+
39
+ @dataclass(slots=True)
40
+ class ProtocolInformation:
41
+ """
42
+ AAS Endpoint의 프로토콜 정보를 나타내는 wire 포맷.
43
+
44
+ 필드 이름이 camelCase인 것은 서버(JSON) 표기를 그대로 따른 것이다.
45
+
46
+ Attributes:
47
+ href (Optional[str]): 엔드포인트 URL.
48
+ endpointProtocol (Optional[str]): 프로토콜 이름 (예: "HTTP").
49
+ endpointProtocolVersion (Optional[str]): 프로토콜 버전 (예: "1.1").
50
+ subprotocol (Optional[str]): 서브프로토콜 이름.
51
+ subprotocolBody (Optional[str]): 서브프로토콜 본문.
52
+ subprotocolBody_encoding (Optional[str]): 서브프로토콜 본문 인코딩.
53
+ securityAttributes (list[SecurityAttributeObject]): 보안 속성 목록.
54
+ """
55
+ href: Optional[str]
56
+ endpointProtocol: Optional[str] = field(default=None)
57
+ endpointProtocolVersion: Optional[str] = field(default=None)
58
+ subprotocol: Optional[str] = field(default=None)
59
+ subprotocolBody: Optional[str] = field(default=None)
60
+ subprotocolBody_encoding: Optional[str] = field(default=None)
61
+ securityAttributes: list[SecurityAttributeObject] = field(default_factory=list)
62
+
63
+
64
+ @dataclass(slots=True)
65
+ class Endpoint:
66
+ """
67
+ AAS Endpoint (interface + protocolInformation 묶음).
68
+
69
+ Attributes:
70
+ interface (str): 인터페이스 식별자 (예: "SUBMODEL").
71
+ protocolInformation (ProtocolInformation): 접속 프로토콜 정보.
72
+ """
73
+ interface: str
74
+ protocolInformation: ProtocolInformation
75
+
76
+
77
+ @dataclass(slots=True)
78
+ class OperationVariable:
79
+ """
80
+ AAS Operation의 입력/출력/입출력 변수를 감싸는 컨테이너.
81
+
82
+ 내부적으로는 basyx의 `model.SubmodelElement`를 그대로 들고 있으며,
83
+ JSON 변환은 `basyx_serde`에 위임한다.
84
+
85
+ Attributes:
86
+ value (model.SubmodelElement): 변수의 값을 담은 SubmodelElement.
87
+ """
88
+ value: model.SubmodelElement
89
+
90
+ @classmethod
91
+ def from_dict(cls, data: dict) -> OperationVariable:
92
+ """
93
+ JSON 객체에서 OperationVariable을 만든다.
94
+
95
+ Args:
96
+ data (dict): `{'value': <SubmodelElement JSON>}` 형식의 dict.
97
+ Returns:
98
+ OperationVariable: basyx로 역직렬화된 변수 객체.
99
+ """
100
+ return cls(value=basyx_serde.from_dict(data['value']))
101
+
102
+ def to_dict(self) -> dict[str, Any]:
103
+ """
104
+ JSON 직렬화 가능한 dict로 변환한다.
105
+
106
+ Returns:
107
+ dict[str, Any]: `{'value': <SubmodelElement JSON dict>}`.
108
+ """
109
+ return {'value': json.loads(basyx_serde.to_json(self.value))}
110
+
111
+
112
+ @dataclass(slots=True)
113
+ class OperationResult:
114
+ """
115
+ AAS Operation 호출 결과.
116
+
117
+ Attributes:
118
+ messages (Optional[list[str]]): 서버가 반환한 메시지 목록.
119
+ execution_state (str): 실행 상태 ("Completed", "Failed" 등).
120
+ success (bool): 성공 여부.
121
+ output_op_variables (Optional[list[OperationVariable]]): 출력 인자 변수.
122
+ inoutput_op_variables (Optional[list[OperationVariable]]): 입출력 인자 변수.
123
+ """
124
+ messages: Optional[list[str]]
125
+ execution_state: str
126
+ success: bool
127
+ output_op_variables: Optional[list[OperationVariable]]
128
+ inoutput_op_variables: Optional[list[OperationVariable]]
129
+
130
+ @classmethod
131
+ def from_dict(cls, data: dict) -> OperationResult:
132
+ """
133
+ 서버 JSON 응답에서 OperationResult를 구성한다.
134
+
135
+ 서버 키 `outputArguments` / `inoutputArguments`를 OperationVariable
136
+ 목록으로 변환한다.
137
+
138
+ Args:
139
+ data (dict): JSON으로 파싱된 응답 본문.
140
+ Returns:
141
+ OperationResult: 변환된 결과 객체.
142
+ """
143
+ output_arguments = data.get('outputArguments')
144
+ if output_arguments:
145
+ output_arguments = [OperationVariable.from_dict(arg) for arg in output_arguments]
146
+ inoutput_arguments = data.get('inoutputArguments')
147
+ if inoutput_arguments:
148
+ inoutput_arguments = [OperationVariable.from_dict(arg) for arg in inoutput_arguments]
149
+
150
+ return cls(
151
+ messages=data.get('messages'),
152
+ execution_state=data['executionState'],
153
+ success=data['success'],
154
+ output_op_variables=output_arguments,
155
+ inoutput_op_variables=inoutput_arguments,
156
+ )
157
+
158
+ @classmethod
159
+ def from_json(cls, json_str: str) -> OperationResult:
160
+ """JSON 문자열로부터 OperationResult를 구성한다."""
161
+ return cls.from_dict(json.loads(json_str))
162
+
163
+
164
+ @dataclass(slots=True)
165
+ class OperationHandle:
166
+ """
167
+ 비동기 Operation 호출이 반환하는 핸들.
168
+
169
+ Attributes:
170
+ handle_id (str): 서버가 발급한 비동기 작업 식별자.
171
+ """
172
+ handle_id: str
173
+
174
+ @classmethod
175
+ def from_json(cls, json_str: str) -> OperationHandle:
176
+ """
177
+ JSON 문자열에서 OperationHandle을 만든다.
178
+
179
+ 서버는 `handleId` 키(camelCase)를 사용하므로 그에 맞춰 매핑한다.
180
+
181
+ Args:
182
+ json_str (str): `{"handleId": "..."}` 형식의 JSON.
183
+ Returns:
184
+ OperationHandle: 핸들 객체.
185
+ """
186
+ json_dict = json.loads(json_str)
187
+ return cls(handle_id=json_dict['handleId'])
188
+
189
+
190
+ @dataclass(slots=True)
191
+ class OperationRequest:
192
+ """
193
+ AAS Operation 호출 요청 본문.
194
+
195
+ Attributes:
196
+ input_arguments (Iterable[OperationVariable]): 입력 인자 변수 목록.
197
+ inoutput_arguments (Iterable[OperationVariable]): 입출력 인자 변수 목록.
198
+ client_timeout_duration (datetime.timedelta): 클라이언트 측 타임아웃.
199
+ JSON 직렬화 시 ISO 8601 duration 문자열로 변환된다.
200
+ """
201
+ input_arguments: Iterable[OperationVariable]
202
+ inoutput_arguments: Iterable[OperationVariable]
203
+ client_timeout_duration: datetime.timedelta
204
+
205
+ def to_json(self) -> str:
206
+ """
207
+ 요청 본문을 JSON 문자열로 직렬화한다.
208
+
209
+ 현재 fa3st 서버는 `inputArguments`만 인식하므로 `inoutputArguments`는
210
+ 직렬화에서 제외한다. (필요 시 아래 주석을 해제하여 포함시킬 수 있다.)
211
+
212
+ Returns:
213
+ str: JSON 문자열.
214
+ """
215
+ in_opv_list = [op_var.to_dict() for op_var in self.input_arguments]
216
+ return json.dumps({
217
+ 'inputArguments': in_opv_list,
218
+ # 'inoutputArguments': [op_var.to_dict() for op_var in self.inoutput_arguments],
219
+ 'clientTimeoutDuration': utils.timedelta_to_iso8601(self.client_timeout_duration),
220
+ })
@@ -1,11 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass, field
4
3
  from typing import Optional, TypedDict
5
4
  from typing_extensions import NotRequired
6
5
 
7
6
  from abc import ABC, abstractmethod
8
7
 
8
+ import logging
9
+ logger = logging.getLogger(__name__)
10
+
9
11
  from ..reference import ElementReference
10
12
  from ..operation import OperationSubmodelService
11
13
  from .argument_spec import ElementReferenceArgumentSpec, LiteralArgumentSpec, TaskOutputArgumentSpec
@@ -58,6 +60,7 @@ class AASOperationTaskInvocation(Invocation):
58
60
  self.argument_specs = argument_specs
59
61
 
60
62
  def run(self, context:Optional[DagContext]=None) -> None:
63
+ logger.info(f"Invoking operation {self.instance}:{self.submodel} with arguments {self.argument_specs}")
61
64
  if context is None:
62
65
  context = AirflowDagContext()
63
66
 
@@ -75,21 +78,3 @@ class AASOperationTaskInvocation(Invocation):
75
78
  return ( f"{self.__class__.__name__}(task={self.instance}:{self.submodel}, "
76
79
  f"inputs={self.argument_specs['inputs']}, "
77
80
  f"outputs={get_output_argument_specs(self.argument_specs)})" )
78
-
79
-
80
- # def add_task_output_values(context:DagContext, out_arg_values:ElementValueDict,
81
- # out_arg_specs_dict:dict[str, OutputArgumentSpecType]) -> None:
82
- # task_id = context.task_id
83
- # for arg_id, arg_value in out_arg_values.items():
84
- # if arg_id not in out_arg_specs_dict:
85
- # continue
86
-
87
- # out_arg_specs = out_arg_specs_dict[arg_id]
88
- # if isinstance(out_arg_specs, list):
89
- # for arg_spec in out_arg_specs:
90
- # if isinstance(arg_spec, OutputArgumentSpec):
91
- # context.add_task_output_value(task_id, arg_id, arg_value)
92
- # break
93
- # else:
94
- # if isinstance(out_arg_specs, OutputArgumentSpec):
95
- # context.add_task_output_value(task_id, arg_id, arg_value)