mdtpy 0.2.2__tar.gz → 0.2.3__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.3/PKG-INFO +132 -0
- mdtpy-0.2.3/README.md +116 -0
- mdtpy-0.2.3/pyproject.toml +40 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/__init__.py +1 -1
- mdtpy-0.2.3/src/mdtpy/aas_misc.py +220 -0
- mdtpy-0.2.3/src/mdtpy/descriptor.py +165 -0
- mdtpy-0.2.3/src/mdtpy/exceptions.py +176 -0
- mdtpy-0.2.3/src/mdtpy/fa3st.py +307 -0
- mdtpy-0.2.3/src/mdtpy/http_client.py +177 -0
- mdtpy-0.2.3/src/mdtpy/instance.py +704 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/operation.py +38 -18
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/parameter.py +24 -18
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/reference.py +5 -10
- mdtpy-0.2.3/src/mdtpy/submodel.py +491 -0
- mdtpy-0.2.3/src/mdtpy/timeseries.py +371 -0
- mdtpy-0.2.3/src/mdtpy/utils.py +253 -0
- mdtpy-0.2.3/src/mdtpy/value.py +363 -0
- mdtpy-0.2.3/src/mdtpy.egg-info/PKG-INFO +132 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy.egg-info/SOURCES.txt +21 -1
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy.egg-info/requires.txt +1 -2
- mdtpy-0.2.3/src/samples/sample_add_and_sleep.py +16 -0
- mdtpy-0.2.3/src/samples/sample_inspector_simiulation.py +113 -0
- mdtpy-0.2.3/src/samples/sample_instance.py +20 -0
- mdtpy-0.2.3/src/samples/sample_multi_ops.py +48 -0
- mdtpy-0.2.3/src/samples/sample_operations.py +23 -0
- mdtpy-0.2.3/src/samples/sample_parameters.py +29 -0
- mdtpy-0.2.3/src/samples/sample_reference.py +37 -0
- mdtpy-0.2.3/src/samples/sample_submodel_elements.py +12 -0
- mdtpy-0.2.3/src/samples/sample_timeseries.py +25 -0
- mdtpy-0.2.3/tests/test_aas_misc.py +254 -0
- mdtpy-0.2.3/tests/test_descriptor.py +338 -0
- mdtpy-0.2.3/tests/test_fa3st.py +389 -0
- mdtpy-0.2.3/tests/test_http_client.py +237 -0
- mdtpy-0.2.3/tests/test_instance.py +604 -0
- mdtpy-0.2.3/tests/test_operation.py +444 -0
- mdtpy-0.2.3/tests/test_parameter.py +168 -0
- mdtpy-0.2.3/tests/test_submodel.py +511 -0
- mdtpy-0.2.3/tests/test_timeseries.py +403 -0
- mdtpy-0.2.3/tests/test_utils.py +301 -0
- mdtpy-0.2.3/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.3}/setup.cfg +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/airflow/__init__.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/airflow/argument_spec.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/airflow/dag_context.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/airflow/invocation.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/basyx/__init__.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/basyx/serde.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy/basyx/utils.py +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy.egg-info/dependency_links.txt +0 -0
- {mdtpy-0.2.2 → mdtpy-0.2.3}/src/mdtpy.egg-info/top_level.txt +0 -0
mdtpy-0.2.3/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mdtpy
|
|
3
|
+
Version: 0.2.3
|
|
4
|
+
Summary: MDTPlatform을 python 언어를 통해 활용할 수 있는 라이브러리를 제공한다.
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: basyx-python-sdk<3.0.0,>=2.0.0
|
|
8
|
+
Requires-Dist: dataclass-wizard<1,>=0.22
|
|
9
|
+
Requires-Dist: isodate>=0.7.2
|
|
10
|
+
Requires-Dist: jinja2>=3.1.6
|
|
11
|
+
Requires-Dist: pandas>=2.3.3
|
|
12
|
+
Requires-Dist: requests>=2.32.5
|
|
13
|
+
Requires-Dist: tika>=3.1.0
|
|
14
|
+
Requires-Dist: typing_extensions>=4.2.0
|
|
15
|
+
Requires-Dist: urllib3<3.0.0,>=2.6.2
|
|
16
|
+
|
|
17
|
+
# mdtpy
|
|
18
|
+
|
|
19
|
+
MDT(Manufacturing Digital Twin) 플랫폼을 위한 Python 클라이언트 라이브러리.
|
|
20
|
+
Asset Administration Shell(AAS) 표준을 기반으로 MDT Instance Manager 및 개별
|
|
21
|
+
FA³ST 인스턴스에 HTTP REST로 접근하는 API를 제공한다.
|
|
22
|
+
|
|
23
|
+
내부적으로 [`basyx-python-sdk`](https://pypi.org/project/basyx-python-sdk/)를
|
|
24
|
+
사용해 AAS 모델(`Property`, `SubmodelElementCollection`, `SubmodelElementList`,
|
|
25
|
+
`File`, `Range`, `MultiLanguageProperty`, `Operation`, `TimeSeries` 등)을 다룬다.
|
|
26
|
+
|
|
27
|
+
## 요구 사항
|
|
28
|
+
|
|
29
|
+
- Python 3.10 이상
|
|
30
|
+
- [uv](https://docs.astral.sh/uv/) (의존성/빌드 관리)
|
|
31
|
+
|
|
32
|
+
## 설치
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv sync # 런타임 의존성
|
|
36
|
+
make install-dev # 개발 의존성(pytest)까지 함께 설치
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
소스 레이아웃은 `src/mdtpy/`이며, `src/`가 Python path에 등록되어 있다
|
|
40
|
+
(`.vscode/settings.json`).
|
|
41
|
+
|
|
42
|
+
## 빠른 시작
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import mdtpy
|
|
46
|
+
|
|
47
|
+
# 1. MDT Instance Manager에 접속
|
|
48
|
+
manager = mdtpy.connect("http://localhost:12985/instance-manager")
|
|
49
|
+
|
|
50
|
+
# 2. 인스턴스 가져오기 / 시작
|
|
51
|
+
instance = manager.instances['my_twin']
|
|
52
|
+
if not instance.is_running():
|
|
53
|
+
instance.start()
|
|
54
|
+
|
|
55
|
+
# 3. 파라미터 읽기/쓰기
|
|
56
|
+
param = instance.parameters['Status']
|
|
57
|
+
print(param.read_value())
|
|
58
|
+
param.update_value('Running')
|
|
59
|
+
|
|
60
|
+
# 4. Operation 호출
|
|
61
|
+
op = instance.operations['Inspect']
|
|
62
|
+
result = op.invoke(Image=instance.parameters['UpperImage'])
|
|
63
|
+
op.output_arguments.update_value(result)
|
|
64
|
+
|
|
65
|
+
# 5. 시계열 데이터
|
|
66
|
+
ts = instance.timeseries['WelderAmpereLog'].timeseries()
|
|
67
|
+
df = ts.segments['Latest'].records_as_pandas()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
상세 사용법은 [`doc/programming_guide.md`](doc/programming_guide.md) 참조.
|
|
71
|
+
|
|
72
|
+
## 주요 모듈
|
|
73
|
+
|
|
74
|
+
| 모듈 | 역할 |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `mdtpy.instance` | `connect()`, `MDTInstanceManager`, `MDTInstance`, 컬렉션, 폴러 |
|
|
77
|
+
| `mdtpy.reference` | `ElementReference` 추상화 (`DefaultElementReference`, `LazyElementReference`) |
|
|
78
|
+
| `mdtpy.parameter` | `MDTParameter`, `MDTParameterCollection` |
|
|
79
|
+
| `mdtpy.submodel` | `SubmodelService`, `SubmodelServiceCollection`, `SubmodelElementCollection` |
|
|
80
|
+
| `mdtpy.operation` | `OperationSubmodelService`, `Argument`, `ArgumentList` |
|
|
81
|
+
| `mdtpy.timeseries` | `TimeSeriesService` (pandas 통합) |
|
|
82
|
+
| `mdtpy.value` | SME ↔ Python 값 ↔ 서버 wire JSON 변환 |
|
|
83
|
+
| `mdtpy.descriptor` | 불변 dataclass 디스크립터, semantic_id 기반 분류 |
|
|
84
|
+
| `mdtpy.aas_misc` | AAS wire 포맷 dataclass (`Endpoint`, `OperationVariable` 등) |
|
|
85
|
+
| `mdtpy.fa3st` | 개별 FA³ST 인스턴스용 HTTP 헬퍼 (`call_get`/`call_put`/...) |
|
|
86
|
+
| `mdtpy.http_client` | Instance Manager용 응답 파서, 공통 예외 변환 |
|
|
87
|
+
| `mdtpy.exceptions` | `MDTException` 계층 |
|
|
88
|
+
| `mdtpy.utils` | ISO 8601 / timedelta / SME→Python 변환 헬퍼 |
|
|
89
|
+
| `mdtpy.airflow` | Apache Airflow DAG 통합 (선택, 자동 import되지 않음) |
|
|
90
|
+
| `mdtpy.basyx.serde` | basyx-python-sdk 직렬화 래퍼 |
|
|
91
|
+
|
|
92
|
+
## 개발
|
|
93
|
+
|
|
94
|
+
### 테스트 실행
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
make test # 전체 pytest suite
|
|
98
|
+
make test-cov # 커버리지 보고서 포함
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
또는 직접 실행:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
uv run --env-file .env pytest tests/test_instance.py
|
|
105
|
+
uv run --env-file .env pytest tests/test_instance.py::TestMDTInstanceCollection -v
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
> **참고**: ROS2(`/opt/ros/humble/...`)를 source한 셸에서는 시스템
|
|
109
|
+
> `launch_pytest` 플러그인이 자동 로드되어 `yaml` 누락으로 충돌한다. `Makefile`
|
|
110
|
+
> 과 `.env`가 `PYTEST_DISABLE_PLUGIN_AUTOLOAD=1`을 주입하여 이를 우회한다.
|
|
111
|
+
> ROS가 source되지 않은 셸이라면 `uv run pytest`만으로도 충분하다.
|
|
112
|
+
|
|
113
|
+
`tests/` 폴더의 단위 테스트는 외부 서버 의존 없이 mock으로 동작한다
|
|
114
|
+
(300+ tests). `src/samples/sample_*.py` 스크립트들은 **실서버 대상 사용 예제 /
|
|
115
|
+
스모크 테스트**이므로 별도 환경에서 실행한다 (예: `python src/samples/sample_reference.py`).
|
|
116
|
+
|
|
117
|
+
### 코드 스타일
|
|
118
|
+
|
|
119
|
+
- 코드 주석/docstring: 한국어 (평서문 "~한다")
|
|
120
|
+
- 로깅/예외 메시지: 영어
|
|
121
|
+
- import 순서: `__future__` → `typing` → 표준 → 서드파티 → 로컬
|
|
122
|
+
- 타입 힌트: built-in 우선 (`list`/`dict`/`tuple`), `Optional[X]` 권장
|
|
123
|
+
- 들여쓰기: 4-space
|
|
124
|
+
- 라인 길이: 100자 (신규 코드)
|
|
125
|
+
|
|
126
|
+
자세한 규칙과 아키텍처는 [`CLAUDE.md`](CLAUDE.md) 참조.
|
|
127
|
+
|
|
128
|
+
## 빌드
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uv build # sdist + wheel을 dist/에 생성
|
|
132
|
+
```
|
mdtpy-0.2.3/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,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mdtpy"
|
|
7
|
+
version = "0.2.3"
|
|
8
|
+
description = "MDTPlatform을 python 언어를 통해 활용할 수 있는 라이브러리를 제공한다."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"basyx-python-sdk>=2.0.0,<3.0.0",
|
|
13
|
+
"dataclass-wizard>=0.22,<1",
|
|
14
|
+
"isodate>=0.7.2",
|
|
15
|
+
"jinja2>=3.1.6",
|
|
16
|
+
"pandas>=2.3.3",
|
|
17
|
+
"requests>=2.32.5",
|
|
18
|
+
"tika>=3.1.0",
|
|
19
|
+
"typing_extensions>=4.2.0",
|
|
20
|
+
"urllib3>=2.6.2,<3.0.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
25
|
+
include = ["mdtpy*"]
|
|
26
|
+
exclude = ["samples*"]
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=8.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
# 참고: ROS2를 source한 쉘에서는 /opt/ros/.../launch_pytest 플러그인이
|
|
36
|
+
# 자동 로드되어 yaml 누락으로 충돌한다. `.env`의 PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
|
|
37
|
+
# 을 uv가 픽업하도록 호출해야 하므로 다음 중 하나를 사용한다.
|
|
38
|
+
# make test
|
|
39
|
+
# uv run --env-file .env pytest
|
|
40
|
+
# 또는 셸에 `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
|
+
})
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
"""MDTInstance의 생명주기 상태."""
|
|
12
|
+
|
|
13
|
+
STOPPED = "STOPPED"
|
|
14
|
+
STARTING = "STARTING"
|
|
15
|
+
RUNNING = "RUNNING"
|
|
16
|
+
STOPPING = "STOPPING"
|
|
17
|
+
FAILED = "FAILED"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MDTAssetType(Enum):
|
|
21
|
+
"""MDTInstance가 표현하는 자산 종류.
|
|
22
|
+
|
|
23
|
+
Note:
|
|
24
|
+
값이 PascalCase인 것은 서버 응답에 사용되는 표기를 그대로 따른 것이다.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
Machine = "Machine"
|
|
28
|
+
Process = "Process"
|
|
29
|
+
Line = "Line"
|
|
30
|
+
Factory = "Factory"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AssetKind(Enum):
|
|
34
|
+
"""AAS Asset Kind (instance / type / not-applicable)."""
|
|
35
|
+
|
|
36
|
+
INSTANCE = "INSTANCE"
|
|
37
|
+
NOT_APPLICABLE = "NOT_APPLICABLE"
|
|
38
|
+
TYPE = "TYPE"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class InstanceDescriptor(JSONWizard):
|
|
43
|
+
"""
|
|
44
|
+
MDTInstance 등록정보.
|
|
45
|
+
|
|
46
|
+
Attributes:
|
|
47
|
+
id (str): MDTInstance의 고유 식별자.
|
|
48
|
+
status (MDTInstanceStatus): MDTInstance의 현재 상태.
|
|
49
|
+
aas_id (str): 연결된 AAS의 ID(URI).
|
|
50
|
+
base_endpoint (Optional[str]): MDTInstance의 베이스 엔드포인트.
|
|
51
|
+
aas_id_short (Optional[str]): 연결된 AAS의 idShort.
|
|
52
|
+
global_asset_id (Optional[str]): 전역 자산 식별자.
|
|
53
|
+
asset_type (Optional[MDTAssetType]): 자산 종류.
|
|
54
|
+
asset_kind (Optional[AssetKind]): AAS Asset Kind.
|
|
55
|
+
"""
|
|
56
|
+
id: str
|
|
57
|
+
status: MDTInstanceStatus
|
|
58
|
+
aas_id: str
|
|
59
|
+
base_endpoint: Optional[str] = field(default=None, hash=False, compare=False)
|
|
60
|
+
aas_id_short: Optional[str] = field(default=None, hash=False, compare=False)
|
|
61
|
+
global_asset_id: Optional[str] = field(default=None, hash=False, compare=False)
|
|
62
|
+
asset_type: Optional[MDTAssetType] = field(default=None, hash=False, compare=False)
|
|
63
|
+
asset_kind: Optional[AssetKind] = field(default=None, hash=False, compare=False)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True, slots=True)
|
|
67
|
+
class MDTParameterDescriptor(JSONWizard):
|
|
68
|
+
"""
|
|
69
|
+
MDTParameter 등록정보.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
id (str): 파라미터 식별자.
|
|
73
|
+
value_type (str): 파라미터 값 타입 (XSD 데이터 타입 문자열).
|
|
74
|
+
reference (str): 파라미터 참조 문자열 (`param:<instance>:<param>` 형태).
|
|
75
|
+
name (Optional[str]): 사람이 읽는 파라미터 이름.
|
|
76
|
+
endpoint (Optional[str]): 파라미터 접근 엔드포인트 URL.
|
|
77
|
+
"""
|
|
78
|
+
id: str
|
|
79
|
+
value_type: str
|
|
80
|
+
reference: str
|
|
81
|
+
name: Optional[str] = None
|
|
82
|
+
endpoint: Optional[str] = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
SEMANTIC_ID_INFOR_MODEL_SUBMODEL = "https://etri.re.kr/mdt/Submodel/InformationModel/1/1"
|
|
86
|
+
SEMANTIC_ID_AI_SUBMODEL = "https://etri.re.kr/mdt/Submodel/AI/1/1"
|
|
87
|
+
SEMANTIC_ID_SIMULATION_SUBMODEL = "https://etri.re.kr/mdt/Submodel/Simulation/1/1"
|
|
88
|
+
SEMANTIC_ID_DATA_SUBMODEL = "https://etri.re.kr/mdt/Submodel/Data/1/1"
|
|
89
|
+
SEMANTIC_ID_TIME_SERIES_SUBMODEL = "https://admin-shell.io/idta/TimeSeries/1/1"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@dataclass(frozen=True, slots=True)
|
|
93
|
+
class MDTSubmodelDescriptor(JSONWizard):
|
|
94
|
+
"""
|
|
95
|
+
MDTSubmodel 등록정보.
|
|
96
|
+
|
|
97
|
+
semantic_id 값에 따라 InformationModel / Data / Simulation / AI / TimeSeries
|
|
98
|
+
중 하나로 분류된다. 분류는 `is_*()` 메서드로 확인한다.
|
|
99
|
+
|
|
100
|
+
Attributes:
|
|
101
|
+
id (str): 서브모델 식별자(URI).
|
|
102
|
+
id_short (str): 서브모델 idShort.
|
|
103
|
+
semantic_id (str): 서브모델 semantic id (URI).
|
|
104
|
+
endpoint (Optional[str]): 서브모델 접근 엔드포인트 URL.
|
|
105
|
+
"""
|
|
106
|
+
id: str
|
|
107
|
+
id_short: str
|
|
108
|
+
semantic_id: str
|
|
109
|
+
endpoint: Optional[str]
|
|
110
|
+
|
|
111
|
+
def is_information_model(self) -> bool:
|
|
112
|
+
"""semantic_id가 Information Model에 해당하면 True를 반환한다."""
|
|
113
|
+
return self.semantic_id == SEMANTIC_ID_INFOR_MODEL_SUBMODEL
|
|
114
|
+
|
|
115
|
+
def is_data(self) -> bool:
|
|
116
|
+
"""semantic_id가 Data Model에 해당하면 True를 반환한다."""
|
|
117
|
+
return self.semantic_id == SEMANTIC_ID_DATA_SUBMODEL
|
|
118
|
+
|
|
119
|
+
def is_simulation(self) -> bool:
|
|
120
|
+
"""semantic_id가 Simulation Model에 해당하면 True를 반환한다."""
|
|
121
|
+
return self.semantic_id == SEMANTIC_ID_SIMULATION_SUBMODEL
|
|
122
|
+
|
|
123
|
+
def is_ai(self) -> bool:
|
|
124
|
+
"""semantic_id가 AI Model에 해당하면 True를 반환한다."""
|
|
125
|
+
return self.semantic_id == SEMANTIC_ID_AI_SUBMODEL
|
|
126
|
+
|
|
127
|
+
def is_time_series(self) -> bool:
|
|
128
|
+
"""semantic_id가 Time Series Model에 해당하면 True를 반환한다."""
|
|
129
|
+
return self.semantic_id == SEMANTIC_ID_TIME_SERIES_SUBMODEL
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass(frozen=True, slots=True)
|
|
133
|
+
class ArgumentDescriptor(JSONWizard):
|
|
134
|
+
"""
|
|
135
|
+
Operation 인자(input / inout / output) 등록정보.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
id (str): 인자 식별자.
|
|
139
|
+
id_short_path (str): 서브모델 내 인자의 idShort 경로
|
|
140
|
+
(예: `Inputs.Speed`, `Out.Result[0]`).
|
|
141
|
+
value_type (str): 인자 값 타입 (XSD 데이터 타입 문자열).
|
|
142
|
+
reference (str): 인자 참조 문자열
|
|
143
|
+
(`oparg:<instance>:<op>:in|out:<arg>` 형태).
|
|
144
|
+
"""
|
|
145
|
+
id: str
|
|
146
|
+
id_short_path: str
|
|
147
|
+
value_type: str
|
|
148
|
+
reference: str
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass(frozen=True, slots=True)
|
|
152
|
+
class MDTOperationDescriptor(JSONWizard):
|
|
153
|
+
"""
|
|
154
|
+
MDTOperation 등록정보.
|
|
155
|
+
|
|
156
|
+
Attributes:
|
|
157
|
+
id (str): 연산 식별자.
|
|
158
|
+
operation_type (str): 연산 타입 (예: "sync", "async").
|
|
159
|
+
input_arguments (list[ArgumentDescriptor]): 입력 인자 목록.
|
|
160
|
+
output_arguments (list[ArgumentDescriptor]): 출력 인자 목록.
|
|
161
|
+
"""
|
|
162
|
+
id: str
|
|
163
|
+
operation_type: str
|
|
164
|
+
input_arguments: list[ArgumentDescriptor]
|
|
165
|
+
output_arguments: list[ArgumentDescriptor]
|