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.
- mdtpy-0.2.4/PKG-INFO +139 -0
- mdtpy-0.2.4/README.md +116 -0
- mdtpy-0.2.4/pyproject.toml +49 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/__init__.py +1 -1
- mdtpy-0.2.4/src/mdtpy/aas_misc.py +220 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/airflow/invocation.py +4 -19
- mdtpy-0.2.4/src/mdtpy/descriptor.py +165 -0
- mdtpy-0.2.4/src/mdtpy/exceptions.py +176 -0
- mdtpy-0.2.4/src/mdtpy/fa3st.py +307 -0
- mdtpy-0.2.4/src/mdtpy/http_client.py +177 -0
- mdtpy-0.2.4/src/mdtpy/instance.py +704 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/operation.py +38 -18
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/parameter.py +24 -18
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/reference.py +5 -10
- mdtpy-0.2.4/src/mdtpy/submodel.py +491 -0
- mdtpy-0.2.4/src/mdtpy/timeseries.py +371 -0
- mdtpy-0.2.4/src/mdtpy/utils.py +253 -0
- mdtpy-0.2.4/src/mdtpy/value.py +364 -0
- mdtpy-0.2.4/src/mdtpy.egg-info/PKG-INFO +139 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy.egg-info/SOURCES.txt +21 -1
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy.egg-info/requires.txt +1 -3
- mdtpy-0.2.4/src/samples/sample_add_and_sleep.py +16 -0
- mdtpy-0.2.4/src/samples/sample_inspector_simiulation.py +113 -0
- mdtpy-0.2.4/src/samples/sample_instance.py +40 -0
- mdtpy-0.2.4/src/samples/sample_multi_ops.py +48 -0
- mdtpy-0.2.4/src/samples/sample_operations.py +23 -0
- mdtpy-0.2.4/src/samples/sample_parameters.py +29 -0
- mdtpy-0.2.4/src/samples/sample_reference.py +37 -0
- mdtpy-0.2.4/src/samples/sample_submodel_elements.py +12 -0
- mdtpy-0.2.4/src/samples/sample_timeseries.py +25 -0
- mdtpy-0.2.4/tests/test_aas_misc.py +254 -0
- mdtpy-0.2.4/tests/test_descriptor.py +338 -0
- mdtpy-0.2.4/tests/test_fa3st.py +389 -0
- mdtpy-0.2.4/tests/test_http_client.py +237 -0
- mdtpy-0.2.4/tests/test_instance.py +604 -0
- mdtpy-0.2.4/tests/test_operation.py +444 -0
- mdtpy-0.2.4/tests/test_parameter.py +168 -0
- mdtpy-0.2.4/tests/test_submodel.py +511 -0
- mdtpy-0.2.4/tests/test_timeseries.py +403 -0
- mdtpy-0.2.4/tests/test_utils.py +301 -0
- mdtpy-0.2.4/tests/test_value.py +370 -0
- mdtpy-0.2.2/PKG-INFO +0 -16
- mdtpy-0.2.2/README.md +0 -0
- mdtpy-0.2.2/pyproject.toml +0 -25
- mdtpy-0.2.2/src/mdtpy/aas_misc.py +0 -102
- mdtpy-0.2.2/src/mdtpy/descriptor.py +0 -171
- mdtpy-0.2.2/src/mdtpy/exceptions.py +0 -83
- mdtpy-0.2.2/src/mdtpy/fa3st.py +0 -169
- mdtpy-0.2.2/src/mdtpy/http_client.py +0 -66
- mdtpy-0.2.2/src/mdtpy/instance.py +0 -610
- mdtpy-0.2.2/src/mdtpy/submodel.py +0 -409
- mdtpy-0.2.2/src/mdtpy/timeseries.py +0 -269
- mdtpy-0.2.2/src/mdtpy/utils.py +0 -120
- mdtpy-0.2.2/src/mdtpy/value.py +0 -228
- mdtpy-0.2.2/src/mdtpy.egg-info/PKG-INFO +0 -16
- {mdtpy-0.2.2 → mdtpy-0.2.4}/setup.cfg +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/airflow/__init__.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/airflow/argument_spec.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/airflow/dag_context.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/basyx/__init__.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/basyx/serde.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy/basyx/utils.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.4}/src/mdtpy.egg-info/dependency_links.txt +0 -0
- {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`도 그대로 동작한다.
|
|
@@ -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)
|