dimine-python-sdk 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. dimine_python_sdk/__init__.py +13 -0
  2. dimine_python_sdk/conn/__init__.py +5 -0
  3. dimine_python_sdk/conn/base_conn_client.py +167 -0
  4. dimine_python_sdk/conn/conn_client.py +382 -0
  5. dimine_python_sdk/docs/guides/data_edit.md +306 -0
  6. dimine_python_sdk/docs/guides/data_get.md +266 -0
  7. dimine_python_sdk/docs/guides/data_types.md +152 -0
  8. dimine_python_sdk/docs/guides/features.md +271 -0
  9. dimine_python_sdk/docs/guides/io.md +157 -0
  10. dimine_python_sdk/docs/index.md +41 -0
  11. dimine_python_sdk/docs/quickstart.md +45 -0
  12. dimine_python_sdk/docs/reference/SUMMARY.md +19 -0
  13. dimine_python_sdk/docs/reference/conn/base_conn_client.md +3 -0
  14. dimine_python_sdk/docs/reference/conn/conn_client.md +3 -0
  15. dimine_python_sdk/docs/reference/lib/algorithm/model_func.md +3 -0
  16. dimine_python_sdk/docs/reference/lib/io/file_conversion.md +3 -0
  17. dimine_python_sdk/docs/reference/lib/io/models.md +3 -0
  18. dimine_python_sdk/docs/reference/lib/io/project_plotting.md +3 -0
  19. dimine_python_sdk/docs/reference/lib/prospecting/block_data.md +3 -0
  20. dimine_python_sdk/docs/reference/lib/prospecting/drill_db.md +3 -0
  21. dimine_python_sdk/docs/reference/lib/prospecting/models.md +3 -0
  22. dimine_python_sdk/docs/reference/lib/prospecting/valuation.md +3 -0
  23. dimine_python_sdk/docs/reference/lib/types/entity.md +3 -0
  24. dimine_python_sdk/docs/reference/lib/types/line.md +3 -0
  25. dimine_python_sdk/docs/reference/lib/types/point.md +3 -0
  26. dimine_python_sdk/docs/reference/logger.md +3 -0
  27. dimine_python_sdk/docs/reference/models/conn.md +3 -0
  28. dimine_python_sdk/docs/reference/models/errors.md +3 -0
  29. dimine_python_sdk/docs/reference/models/types.md +3 -0
  30. dimine_python_sdk/docs/summary.md +6 -0
  31. dimine_python_sdk/lib/__init__.py +20 -0
  32. dimine_python_sdk/lib/algorithm/__init__.py +1 -0
  33. dimine_python_sdk/lib/algorithm/model_func.py +219 -0
  34. dimine_python_sdk/lib/exploitation/__init__.py +0 -0
  35. dimine_python_sdk/lib/io/__init__.py +2 -0
  36. dimine_python_sdk/lib/io/file_conversion.py +143 -0
  37. dimine_python_sdk/lib/io/models.py +180 -0
  38. dimine_python_sdk/lib/io/project_plotting.py +40 -0
  39. dimine_python_sdk/lib/prospecting/__init__.py +3 -0
  40. dimine_python_sdk/lib/prospecting/block_data.py +289 -0
  41. dimine_python_sdk/lib/prospecting/drill_db.py +333 -0
  42. dimine_python_sdk/lib/prospecting/models.py +276 -0
  43. dimine_python_sdk/lib/prospecting/valuation.py +113 -0
  44. dimine_python_sdk/lib/types/__init__.py +4 -0
  45. dimine_python_sdk/lib/types/entity.py +957 -0
  46. dimine_python_sdk/lib/types/line.py +188 -0
  47. dimine_python_sdk/lib/types/point.py +695 -0
  48. dimine_python_sdk/logger.py +7 -0
  49. dimine_python_sdk/models/__init__.py +3 -0
  50. dimine_python_sdk/models/conn.py +53 -0
  51. dimine_python_sdk/models/errors.py +4 -0
  52. dimine_python_sdk/models/types.py +131 -0
  53. dimine_python_sdk/site/404.html +671 -0
  54. dimine_python_sdk/site/assets/_mkdocstrings.css +237 -0
  55. dimine_python_sdk/site/assets/images/favicon.png +0 -0
  56. dimine_python_sdk/site/assets/javascripts/bundle.79ae519e.min.js +16 -0
  57. dimine_python_sdk/site/assets/javascripts/bundle.79ae519e.min.js.map +7 -0
  58. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.ar.min.js +1 -0
  59. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.da.min.js +18 -0
  60. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.de.min.js +18 -0
  61. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.du.min.js +18 -0
  62. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.el.min.js +1 -0
  63. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.es.min.js +18 -0
  64. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.fi.min.js +18 -0
  65. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.fr.min.js +18 -0
  66. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.he.min.js +1 -0
  67. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.hi.min.js +1 -0
  68. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.hu.min.js +18 -0
  69. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.hy.min.js +1 -0
  70. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.it.min.js +18 -0
  71. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.ja.min.js +1 -0
  72. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.jp.min.js +1 -0
  73. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.kn.min.js +1 -0
  74. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.ko.min.js +1 -0
  75. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.multi.min.js +1 -0
  76. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.nl.min.js +18 -0
  77. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.no.min.js +18 -0
  78. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.pt.min.js +18 -0
  79. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.ro.min.js +18 -0
  80. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.ru.min.js +18 -0
  81. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.sa.min.js +1 -0
  82. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.stemmer.support.min.js +1 -0
  83. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.sv.min.js +18 -0
  84. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.ta.min.js +1 -0
  85. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.te.min.js +1 -0
  86. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.th.min.js +1 -0
  87. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.tr.min.js +18 -0
  88. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.vi.min.js +1 -0
  89. dimine_python_sdk/site/assets/javascripts/lunr/min/lunr.zh.min.js +1 -0
  90. dimine_python_sdk/site/assets/javascripts/lunr/tinyseg.js +206 -0
  91. dimine_python_sdk/site/assets/javascripts/lunr/wordcut.js +6708 -0
  92. dimine_python_sdk/site/assets/javascripts/workers/search.2c215733.min.js +42 -0
  93. dimine_python_sdk/site/assets/javascripts/workers/search.2c215733.min.js.map +7 -0
  94. dimine_python_sdk/site/assets/stylesheets/main.484c7ddc.min.css +1 -0
  95. dimine_python_sdk/site/assets/stylesheets/main.484c7ddc.min.css.map +1 -0
  96. dimine_python_sdk/site/assets/stylesheets/palette.ab4e12ef.min.css +1 -0
  97. dimine_python_sdk/site/assets/stylesheets/palette.ab4e12ef.min.css.map +1 -0
  98. dimine_python_sdk/site/guides/data_edit.html +1251 -0
  99. dimine_python_sdk/site/guides/data_get.html +1206 -0
  100. dimine_python_sdk/site/guides/data_types.html +1529 -0
  101. dimine_python_sdk/site/guides/features.html +1217 -0
  102. dimine_python_sdk/site/guides/io.html +1025 -0
  103. dimine_python_sdk/site/index.html +849 -0
  104. dimine_python_sdk/site/objects.inv +0 -0
  105. dimine_python_sdk/site/quickstart.html +859 -0
  106. dimine_python_sdk/site/reference/SUMMARY.html +729 -0
  107. dimine_python_sdk/site/reference/conn/base_conn_client.html +1295 -0
  108. dimine_python_sdk/site/reference/conn/conn_client.html +3551 -0
  109. dimine_python_sdk/site/reference/lib/algorithm/model_func.html +2162 -0
  110. dimine_python_sdk/site/reference/lib/io/file_conversion.html +1725 -0
  111. dimine_python_sdk/site/reference/lib/io/models.html +1625 -0
  112. dimine_python_sdk/site/reference/lib/io/project_plotting.html +984 -0
  113. dimine_python_sdk/site/reference/lib/prospecting/block_data.html +2455 -0
  114. dimine_python_sdk/site/reference/lib/prospecting/drill_db.html +2836 -0
  115. dimine_python_sdk/site/reference/lib/prospecting/models.html +2309 -0
  116. dimine_python_sdk/site/reference/lib/prospecting/valuation.html +1373 -0
  117. dimine_python_sdk/site/reference/lib/types/entity.html +6728 -0
  118. dimine_python_sdk/site/reference/lib/types/line.html +1985 -0
  119. dimine_python_sdk/site/reference/lib/types/point.html +3672 -0
  120. dimine_python_sdk/site/reference/logger.html +751 -0
  121. dimine_python_sdk/site/reference/models/conn.html +751 -0
  122. dimine_python_sdk/site/reference/models/errors.html +802 -0
  123. dimine_python_sdk/site/reference/models/types.html +1679 -0
  124. dimine_python_sdk/site/search/search_index.json +1 -0
  125. dimine_python_sdk/site/sitemap.xml +107 -0
  126. dimine_python_sdk/site/sitemap.xml.gz +0 -0
  127. dimine_python_sdk/site/summary.html +697 -0
  128. dimine_python_sdk-0.1.8.dist-info/METADATA +15 -0
  129. dimine_python_sdk-0.1.8.dist-info/RECORD +135 -0
  130. dimine_python_sdk-0.1.8.dist-info/WHEEL +5 -0
  131. dimine_python_sdk-0.1.8.dist-info/top_level.txt +2 -0
  132. tests/conn_test.py +250 -0
  133. tests/test_base_type.py +143 -0
  134. tests/ws_client.py +60 -0
  135. tests/ws_server.py +271 -0
@@ -0,0 +1,13 @@
1
+ from pathlib import Path
2
+
3
+ from .conn import ConnClient
4
+
5
+ DOCS_PATH = Path(__file__).parent / "docs"
6
+ __VERSION__ = "0.1.8" # 由 build.bat 中的 generate_version.py 自动更新
7
+
8
+
9
+ __all__ = [
10
+ "ConnClient",
11
+ "DOCS_PATH",
12
+ "__VERSION__"
13
+ ]
@@ -0,0 +1,5 @@
1
+ from .conn_client import ConnClient
2
+ from .conn_client import open_client
3
+
4
+ __all__ = ["ConnClient", "open_client"]
5
+
@@ -0,0 +1,167 @@
1
+
2
+ import asyncio
3
+ from dataclasses import dataclass,field
4
+ import inspect
5
+ import json
6
+ from dimine_python_sdk.logger import logger
7
+ from typing import Any, Dict, List, Optional, Tuple, Callable
8
+ import websockets
9
+ import traceback
10
+
11
+ from dimine_python_sdk.models.conn import ConnEvent, ConnRequest, ConnResponse,CommandType
12
+ from dimine_python_sdk.models.errors import ResponseError
13
+
14
+ @dataclass
15
+ class BaseConnClient:
16
+ model_config = {"arbitrary_types_allowed": True}
17
+
18
+ url: Optional[str] = "ws://localhost:8000"
19
+ is_connected: Optional[bool] = False
20
+ pending_requests: Dict[str, Tuple[asyncio.Future, Callable]] = field(default_factory=dict)
21
+ event_handlers: Dict[str, List[Callable]] = field(default_factory=dict)
22
+ reconnect_delay: int = 2
23
+ max_reconnect_attempts: int = 5
24
+ reconnect_attempts: int = 0
25
+ websocket: websockets.connect = None
26
+ receive_task: Optional[asyncio.Task] = None
27
+
28
+ async def _handle_message(self, message: str):
29
+ """处理接收到的消息"""
30
+ try:
31
+ data = json.loads(message)
32
+ except json.JSONDecodeError:
33
+ logger.error(f"unable parse message: {message}")
34
+ try:
35
+ message_type = data.get("type")
36
+
37
+ if message_type == "response":
38
+ res = ConnResponse(**data)
39
+ # 处理响应消息
40
+ request_id = res.request_id
41
+ if request_id in self.pending_requests:
42
+ future,callback = self.pending_requests.pop(request_id)
43
+ future.set_result(res)
44
+ if callback:
45
+ callback(res)
46
+
47
+ elif message_type == "event":
48
+ event = ConnEvent(**data)
49
+ await self._handle_event(event)
50
+ except Exception as e:
51
+ logger.error(f"error in handle message: {traceback.format_exc()}")
52
+
53
+ async def _handle_event(self, event: ConnEvent):
54
+ """处理事件"""
55
+ if event.event_type in self.event_handlers:
56
+ tasks = []
57
+ for handler in self.event_handlers.get(event.event_type, []):
58
+ if inspect.iscoroutinefunction(handler):
59
+ t_handler = handler(event)
60
+ tasks.append(t_handler)
61
+ else:
62
+ t_handler = asyncio.to_thread(handler, event)
63
+ tasks.append(t_handler)
64
+ await asyncio.gather(*tasks)
65
+ else:
66
+ logger.info(f"no handler for event type : {event.event_type}")
67
+
68
+
69
+ async def _receive_messages(self):
70
+ while self.is_connected:
71
+ try:
72
+ message = await self.websocket.recv()
73
+ await self._handle_message(message)
74
+ except websockets.exceptions.ConnectionClosed:
75
+ logger.error("WebSocket connection closed")
76
+ self.is_connected = False
77
+ await self._reconnect()
78
+ except Exception as e:
79
+ logger.error(f"error in receive or handle message: {e}")
80
+
81
+
82
+ async def connect(self)-> bool:
83
+ try:
84
+ self.websocket = await websockets.connect(self.url)
85
+ self.is_connected = True
86
+ self.reconnect_attempts = 0
87
+ logger.info("success connected to %s", self.url)
88
+ if self.receive_task is None or self.receive_task.done():
89
+ self.receive_task = asyncio.create_task(self._receive_messages())
90
+ return True
91
+ except Exception as e:
92
+ logger.error("failed to connect to %s: %s", self.url, e)
93
+ self.is_connected = False
94
+ self.reconnect_attempts += 1
95
+ if self.reconnect_attempts < self.max_reconnect_attempts:
96
+ await asyncio.sleep(self.reconnect_delay)
97
+ return await self.connect()
98
+ return False
99
+
100
+
101
+ async def _reconnect(self)->bool:
102
+ """重新连接到服务器"""
103
+ if self.reconnect_attempts < self.max_reconnect_attempts:
104
+ self.reconnect_attempts += 1
105
+ logger.info(f"try to reconnect ({self.reconnect_attempts}/{self.max_reconnect_attempts})...")
106
+ await asyncio.sleep(self.reconnect_delay)
107
+ return await self.connect()
108
+ else:
109
+ logger.error("max reconnect attempts reached")
110
+ return False
111
+
112
+ async def _send_request(self, request: ConnRequest, callback: Callable = None)->ConnResponse:
113
+ if not self.is_connected:
114
+ logger.error("websocket not connected, try to reconnect")
115
+ if not await self.connect():
116
+ raise Exception("failed to reconnect websocket")
117
+ future = asyncio.Future()
118
+ self.pending_requests[request.request_id] = (future, callback)
119
+ try:
120
+ # 发送请求
121
+ await self.websocket.send(request.model_dump_json())
122
+ except Exception as e:
123
+ # 移除pending请求
124
+ if request.request_id in self.pending_requests:
125
+ self.pending_requests.pop(request.request_id)
126
+ raise e
127
+ else:
128
+ result = await future
129
+ return result
130
+
131
+ async def send_request(self, command: CommandType, payload:Optional[Any]=None, callback: Optional[Callable] = None)->ConnResponse:
132
+ """发送请求并等待响应"""
133
+ request = ConnRequest(command=command, payload=payload)
134
+ return await self._send_request(request, callback)
135
+
136
+ async def close(self):
137
+ """关闭WebSocket连接"""
138
+ if self.is_connected:
139
+ self.is_connected = False
140
+ if self.websocket:
141
+ try:
142
+ await self.websocket.close()
143
+ except:
144
+ pass
145
+ if self.receive_task and not self.receive_task.done():
146
+ self.receive_task.cancel()
147
+ logger.info("WebSocket closed.")
148
+
149
+
150
+ def on_event(self, event_type: str, handler: Optional[Callable] = None):
151
+ """注册事件处理器,支持装饰器语法"""
152
+ # 如果提供了handler参数,直接注册
153
+ if handler is not None:
154
+ if event_type not in self.event_handlers:
155
+ self.event_handlers[event_type] = []
156
+ self.event_handlers[event_type].append(handler)
157
+ return handler
158
+
159
+ # 否则返回装饰器函数
160
+ def decorator(func: Callable) -> Callable:
161
+ if event_type not in self.event_handlers:
162
+ self.event_handlers[event_type] = []
163
+ self.event_handlers[event_type].append(func)
164
+ return func
165
+
166
+ return decorator
167
+
@@ -0,0 +1,382 @@
1
+
2
+ from contextlib import asynccontextmanager
3
+ from pathlib import Path
4
+ from typing import Any, AsyncGenerator, Dict, List, Optional
5
+ import numpy as np
6
+ from dimine_python_sdk.conn.base_conn_client import BaseConnClient
7
+ from dimine_python_sdk.models import FileInfo, ResponseStatus, CommandType,LayerInfo, FeatureInfo
8
+
9
+ from dimine_python_sdk.models.types import GEOMETRY, Line, Point, Shell
10
+
11
+
12
+ class ConnClient(BaseConnClient):
13
+
14
+ async def get_files(self) -> List[FileInfo]:
15
+ """获取文件对象树"""
16
+ res = await self.send_request(CommandType.GET_FILE_TREE)
17
+ if res.status == ResponseStatus.FAIL:
18
+ raise Exception(res.error)
19
+ return [FileInfo(**item, related=True) for item in res.payload]
20
+
21
+ async def close_file(self, file_id: str):
22
+ """关闭文件"""
23
+ res = await self.send_request(CommandType.FILE_CLOSE, {"file": file_id})
24
+ if res.status == ResponseStatus.FAIL:
25
+ raise Exception(res.error)
26
+
27
+ async def open_file(self, file_path: str| Path) -> FileInfo:
28
+ """打开文件"""
29
+ if isinstance(file_path, str): # 如果是 str 对象,则转换为 Path
30
+ file_path = Path(file_path)
31
+ if not file_path.exists(): # 如果文件不存在,报错
32
+ raise Exception(f"file {file_path} not exists")
33
+ res = await self.send_request(CommandType.FILE_OPEN, {"file_path": str(file_path)})
34
+ if res.status == ResponseStatus.FAIL:
35
+ raise Exception(res.error)
36
+ print("res.payload: ", res.payload)
37
+ if res.payload:
38
+ return FileInfo(id=res.payload[0].get("file_id"), name=res.payload[0].get("file_id"), related=True)
39
+ else:
40
+ raise Exception("file already opened!")
41
+
42
+
43
+ async def export_file(self, file_id: str, file_path: str| Path, mkdir: bool = True):
44
+ """导出文件
45
+
46
+ Args:
47
+ file_id: 文件ID或文件名称
48
+ file_path: 导出目标路径
49
+ mkdir: 是否自动创建目录
50
+ Raises:
51
+ Exception: 导出失败时抛出异常
52
+ """
53
+ if isinstance(file_path, str): # 如果是 str 对象,则转换为 Path
54
+ file_path = Path(file_path)
55
+ if not file_path.parent.exists(): # 如果目录不存在,报错
56
+ if mkdir:
57
+ file_path.parent.mkdir(parents=True, exist_ok=True)
58
+ else:
59
+ raise Exception(f"path {file_path.parent} not exists")
60
+ res = await self.send_request(CommandType.FILE_EXPORT, {"file": file_id, "file_path": str(file_path)})
61
+ if res.status == ResponseStatus.FAIL:
62
+ raise Exception(res.error)
63
+
64
+ # 模型相关方法
65
+ async def create_geometry(self, geometries: List[GEOMETRY]) -> None:
66
+ """创建模型
67
+
68
+ Args:
69
+ geometries: 模型列表,每个模型包含以下字段:
70
+ - file: 文件名称
71
+ - layer: 图层名称
72
+ - feature: 要素列名称
73
+ - entity_type: "point | line | polyline | shell"
74
+ - geometry: 几何信息 (coordinates)
75
+ - properties: 属性信息列表
76
+ - color: 颜色 [R, G, B]
77
+
78
+ Raises:
79
+ Exception: 创建失败时抛出异常
80
+ """
81
+ # 格式化模型数据
82
+ formatted_geometries = []
83
+ for geom in geometries:
84
+ formatted_geom = {
85
+ "file": geom.file,
86
+ "layer": geom.layer,
87
+ "feature": geom.feature,
88
+ "entity_type": geom.type,
89
+ "geometry": geom.geometry.astype("str").tolist() if geom.type!="shell" else {"indexs": geom.geometry.faces.tolist(), "points": geom.geometry.points.astype("str").tolist()},
90
+ "color": geom.color,
91
+ }
92
+
93
+ # 处理属性信息
94
+ formatted_geom["properties"] = [
95
+ {"name": prop.name, "value": prop.value}
96
+ for prop in geom.properties
97
+ ]
98
+
99
+ formatted_geometries.append(formatted_geom)
100
+ response = await self.send_request(CommandType.CREATE_GEOMETRY, formatted_geometries)
101
+ if response.status == ResponseStatus.FAIL:
102
+ raise Exception(response.error)
103
+ # 更新id等信息
104
+ for i, item in enumerate(response.payload):
105
+ geometries[i].id=item.get("id")
106
+ geometries[i].related=True
107
+
108
+
109
+ async def get_geometry(self, file_id: str, layer_id: str, entity_types: List[str]=[], features: Optional[List[str]] = None) -> List[GEOMETRY]:
110
+ """获取模型"""
111
+ # 先获取要素信息,
112
+ all_features = await self.get_features(file_id)
113
+
114
+ def _get_property_type(feature_name:str, name: str)-> str:
115
+ """获取属性类型"""
116
+ for feature in all_features:
117
+ if feature.name == feature_name:
118
+ for property in feature.properties:
119
+ if property.name == name:
120
+ return property.type
121
+
122
+ payload = {
123
+ "file": file_id,
124
+ "layer": layer_id,
125
+ "entity_type": entity_types
126
+ }
127
+ if features:
128
+ payload["feature"] = features
129
+
130
+ response = await self.send_request(CommandType.GET_GEOMETRY, payload)
131
+ if response.status == ResponseStatus.FAIL:
132
+ raise Exception(response.error)
133
+
134
+ def _format_value(type:str, value: Any):
135
+ if value == "" and type != "string":
136
+ return None
137
+ elif type == "double":
138
+ return float(value)
139
+ elif type == "string":
140
+ return str(value)
141
+ elif type in ["int","long","short"]:
142
+ return int(value)
143
+ else:
144
+ raise Exception(f"unknown property type: {type}")
145
+
146
+ def _handle_property(feature_name:str, properties: List[Dict[str, Any]]):
147
+ for property in properties:
148
+ property["type"] = _get_property_type(feature_name, property.get("name"))
149
+ property["value"] = _format_value(property.get("type"), property.get("value"))
150
+
151
+ geometries = []
152
+ for item in response.payload:
153
+ _handle_property(item.get("feature"), item.get("properties", []))
154
+ if isinstance(item.get("geometry", []), list):
155
+ tdata = np.asarray(item.get("geometry", [])).astype(np.float64)
156
+ if len(tdata.shape) == 1:
157
+ item["geometry"] = tdata
158
+ geometries.append(Point(**item, related=True))
159
+ elif len(tdata.shape) == 2:
160
+ geometries.append(Line(**item, related=True))
161
+ elif isinstance(item.get("geometry", []), dict):
162
+ item["geometry"]["faces"] = item["geometry"].pop("indexs") if "indexs" in item["geometry"] else []
163
+ geometries.append(Shell(**item, related=True))
164
+ for geom in geometries:
165
+ geom.file = file_id
166
+ geom.layer = layer_id
167
+ return geometries
168
+
169
+ async def update_geometry(self, geometries: List[GEOMETRY]) -> None:
170
+ """更新模型
171
+
172
+ Args:
173
+ geometries: 模型列表,每个模型包含以下字段:
174
+ - id: 模型ID (句柄ID)
175
+ - layer: 图层名称
176
+ - entity_type: "point | line | polyline | shell"
177
+ - geometry: 模型几何信息 (可选)
178
+ - properties: 模型属性信息 (可选)
179
+ - color: 颜色 [R, G, B] (可选)
180
+ - feature: 模型关联要素列表 (可选)
181
+
182
+ Raises:
183
+ Exception: 更新失败时抛出异常
184
+ """
185
+ # 格式化模型数据
186
+ formatted_geometries = []
187
+ for geom in geometries:
188
+ formatted_geom = {
189
+ "id": geom.id,
190
+ "file": geom.file,
191
+ "layer": geom.layer,
192
+ "feature": geom.feature,
193
+ "entity_type": geom.type,
194
+ "geometry": geom.geometry.astype("str").tolist() if geom.type!="shell" else {"indexs": geom.geometry.faces.tolist(), "points": geom.geometry.points.astype("str").tolist()},
195
+ "color": geom.color,
196
+ }
197
+
198
+ # 处理属性信息
199
+ formatted_geom["properties"] = [
200
+ {"name": prop.name, "value": prop.value}
201
+ for prop in geom.properties
202
+ ]
203
+ formatted_geometries.append(formatted_geom)
204
+
205
+ response = await self.send_request(CommandType.UPDATE_GEOMETRY, formatted_geometries)
206
+ if response.status == ResponseStatus.FAIL:
207
+ raise Exception(response.error)
208
+
209
+ async def delete_geometry(self, delete_items: List[Dict[str, str]]) -> None:
210
+ """删除模型
211
+
212
+ Args:
213
+ delete_items: 删除项列表,每个项包含:
214
+ - id: 模型ID (句柄ID)
215
+ - file: 文件ID (从哪个文件删除模型)
216
+
217
+ Raises:
218
+ Exception: 删除失败时抛出异常
219
+ """
220
+ response = await self.send_request(CommandType.DELETE_GEOMETRY, delete_items)
221
+ if response.status == ResponseStatus.FAIL:
222
+ raise Exception(response.error)
223
+
224
+
225
+
226
+ async def update_or_create_geometry(self, geometries: List[GEOMETRY] | GEOMETRY) -> None:
227
+ """创建或更新多个模型, 如果指定了file_id和layer_id, 则将模型添加到指定的文件及图层中"""
228
+ update_geometries = [g for g in geometries if g.related]
229
+ create_geometries = [g for g in geometries if not g.related]
230
+ if update_geometries:
231
+ await self.update_geometry(update_geometries)
232
+ if create_geometries:
233
+ await self.create_geometry(create_geometries)
234
+
235
+
236
+ # 图层相关方法
237
+ async def create_layer(self, file_id: str, name: str) -> LayerInfo:
238
+ """创建图层
239
+
240
+ Args:
241
+ file_id: 文件ID或文件名称
242
+ name: 图层名称
243
+
244
+ Returns:
245
+ 创建的图层信息
246
+
247
+ Raises:
248
+ Exception: 创建失败时抛出异常
249
+ """
250
+ payload = {
251
+ "file": file_id,
252
+ "name": name
253
+ }
254
+ response = await self.send_request(CommandType.CREATE_LAYER, payload)
255
+ if response.status == ResponseStatus.FAIL:
256
+ raise Exception(response.error)
257
+ return LayerInfo(id=name, name=name, related=True)
258
+
259
+
260
+ async def get_layers(self, file_id: str) -> List[LayerInfo]:
261
+ """获取图层"""
262
+ response = await self.send_request(CommandType.GET_LAYERS, {"file": file_id})
263
+ if response.status == ResponseStatus.FAIL:
264
+ raise Exception(response.error)
265
+ return [LayerInfo(**item, related=True) for item in response.payload]
266
+
267
+ async def delete_layer(self, file_id: str, layer_name: str) -> None:
268
+ """删除图层
269
+
270
+ Args:
271
+ file_id: 文件ID或文件名称
272
+ layer_name: 图层名称
273
+
274
+ Raises:
275
+ Exception: 删除失败时抛出异常
276
+ """
277
+ payload = {
278
+ "file": file_id,
279
+ "name": layer_name
280
+ }
281
+ response = await self.send_request(CommandType.DELETE_LAYER, payload)
282
+ if response.status == ResponseStatus.FAIL:
283
+ raise Exception(response.error)
284
+
285
+ # 要素相关方法
286
+ async def create_feature(self, file_id: str, feature_name: str,
287
+ feature_define: List[Dict[str, str]]) -> None:
288
+ """添加要素
289
+
290
+ Args:
291
+ file_id: 文件ID、文件名称
292
+ feature_name: 要素名称
293
+ feature_define: 属性定义列表, 每个元素包含:
294
+ - name: 属性名称
295
+ - type: 属性类型 ("string", "float", "int", "long", "short")
296
+
297
+ Raises:
298
+ Exception: 创建失败时抛出异常
299
+ """
300
+ payload = {
301
+ "file": file_id,
302
+ "feature_name": feature_name,
303
+ "feature_define": feature_define
304
+ }
305
+ response = await self.send_request(CommandType.CREATE_FEATURE, payload)
306
+ if response.status == ResponseStatus.FAIL:
307
+ raise Exception(response.error)
308
+
309
+
310
+ async def get_features(self, file_id: str) -> List[FeatureInfo]:
311
+ """获取要素
312
+
313
+ Args:
314
+ file_id: 文件ID
315
+
316
+ Returns:
317
+ 要素信息列表
318
+
319
+ Raises:
320
+ Exception: 获取失败时抛出异常
321
+ """
322
+ response = await self.send_request(CommandType.GET_FEATURES, {"file": file_id})
323
+ if response.status == ResponseStatus.FAIL:
324
+ raise Exception(response.error)
325
+ return [FeatureInfo(**item, related=True) for item in response.payload]
326
+
327
+ async def update_feature(self, file_id: str, feature_name: str,
328
+ feature_define: List[Dict[str, str]]) -> None:
329
+ """更新要素
330
+
331
+ Args:
332
+ file_id: 文件名称或文件ID
333
+ feature_name: 要素名称
334
+ feature_define: 属性定义列表, 每个元素包含:
335
+ - name: 属性名称
336
+ - type: 属性类型 ("string", "float", "int", "long", "short")
337
+
338
+ Raises:
339
+ Exception: 更新失败时抛出异常
340
+ """
341
+ payload = {
342
+ "file": file_id,
343
+ "feature_name": feature_name,
344
+ "feature_define": feature_define
345
+ }
346
+ response = await self.send_request(CommandType.UPDATE_FEATURE, payload)
347
+ if response.status == ResponseStatus.FAIL:
348
+ raise Exception(response.error)
349
+
350
+ async def delete_feature(self, file: str, feature_name: str) -> None:
351
+ """删除要素
352
+
353
+ Args:
354
+ file: 文件名称
355
+ feature_name: 要素名称
356
+
357
+ Raises:
358
+ Exception: 删除失败时抛出异常
359
+ """
360
+ payload = {
361
+ "file": file,
362
+ "feature_name": feature_name
363
+ }
364
+ response = await self.send_request(CommandType.DELETE_FEATURE, payload)
365
+ if response.status == ResponseStatus.FAIL:
366
+ raise Exception(response.error)
367
+
368
+
369
+
370
+ async def get_selected(self)->List[GEOMETRY]:
371
+ """获取当前选中的模型"""
372
+ pass
373
+
374
+
375
+
376
+
377
+ @asynccontextmanager
378
+ async def open_client(url:Optional[str]=None)->AsyncGenerator[ConnClient,None]:
379
+ client = ConnClient(url=url or "ws://localhost:8000")
380
+ await client.connect()
381
+ yield client
382
+ await client.close()