dimine-python-sdk 0.2.2__py3-none-any.whl → 0.2.3__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 (31) hide show
  1. dimine_python_sdk/__init__.py +24 -4
  2. dimine_python_sdk/cli/commands/camera.py +3 -4
  3. dimine_python_sdk/cli/commands/entity.py +5 -6
  4. dimine_python_sdk/cli/commands/feature.py +5 -6
  5. dimine_python_sdk/cli/commands/file.py +19 -7
  6. dimine_python_sdk/cli/commands/geometry.py +5 -5
  7. dimine_python_sdk/cli/commands/laneway.py +3 -3
  8. dimine_python_sdk/cli/commands/layer.py +5 -6
  9. dimine_python_sdk/cli/commands/text.py +4 -4
  10. dimine_python_sdk/cli/utils/_utils.py +66 -24
  11. dimine_python_sdk/lib/__init__.py +19 -4
  12. dimine_python_sdk/lib/native/__init__.py +4 -0
  13. dimine_python_sdk/models/types.py +9 -9
  14. dimine_python_sdk/site/objects.inv +0 -0
  15. dimine_python_sdk/site/reference/cli/commands/camera.html +11 -11
  16. dimine_python_sdk/site/reference/cli/commands/entity.html +18 -18
  17. dimine_python_sdk/site/reference/cli/commands/feature.html +19 -19
  18. dimine_python_sdk/site/reference/cli/commands/file.html +95 -75
  19. dimine_python_sdk/site/reference/cli/commands/geometry.html +4 -4
  20. dimine_python_sdk/site/reference/cli/commands/laneway.html +2 -2
  21. dimine_python_sdk/site/reference/cli/commands/layer.html +19 -19
  22. dimine_python_sdk/site/reference/cli/commands/text.html +6 -2
  23. dimine_python_sdk/site/reference/models/types.html +8 -8
  24. dimine_python_sdk/site/search/search_index.json +1 -1
  25. dimine_python_sdk/site/sitemap.xml +82 -82
  26. dimine_python_sdk/site/sitemap.xml.gz +0 -0
  27. {dimine_python_sdk-0.2.2.dist-info → dimine_python_sdk-0.2.3.dist-info}/METADATA +1 -1
  28. {dimine_python_sdk-0.2.2.dist-info → dimine_python_sdk-0.2.3.dist-info}/RECORD +31 -31
  29. {dimine_python_sdk-0.2.2.dist-info → dimine_python_sdk-0.2.3.dist-info}/WHEEL +0 -0
  30. {dimine_python_sdk-0.2.2.dist-info → dimine_python_sdk-0.2.3.dist-info}/entry_points.txt +0 -0
  31. {dimine_python_sdk-0.2.2.dist-info → dimine_python_sdk-0.2.3.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,33 @@
1
- from pathlib import Path
1
+ from __future__ import annotations
2
2
 
3
- from .conn import ConnClient
3
+ from pathlib import Path
4
+ from typing import Any
4
5
 
5
6
  DOCS_PATH = Path(__file__).parent / "docs"
6
- __VERSION__ = "0.2.2" # 由 build.bat 中的 generate_version.py 自动更新
7
+ __VERSION__ = "0.2.3" # 由 build.bat 中的 generate_version.py 自动更新
8
+
9
+
10
+ _LAZY_IMPORTS: dict[str, Any] = {}
11
+
12
+
13
+ def __getattr__(name: str) -> Any:
14
+ """PEP 562: 惰性加载 ConnClient,避免导入期触发 numpy/pydantic/websockets 等重依赖。"""
15
+ if name == "ConnClient":
16
+ if name not in _LAZY_IMPORTS:
17
+ from dimine_python_sdk.conn import ConnClient as _ConnClient
18
+
19
+ _LAZY_IMPORTS[name] = _ConnClient
20
+ return _LAZY_IMPORTS[name]
21
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
22
+
23
+
24
+ def __dir__() -> list[str]:
25
+ """确保 dir() 和 IDE 自动补全能看到惰性加载的属性。"""
26
+ return sorted(set(list(globals()) + __all__ + list(_LAZY_IMPORTS)))
7
27
 
8
28
 
9
29
  __all__ = [
10
30
  "ConnClient",
11
31
  "DOCS_PATH",
12
- "__VERSION__"
32
+ "__VERSION__",
13
33
  ]
@@ -1,7 +1,5 @@
1
1
  """相机与截图相关 CLI 命令。"""
2
2
 
3
- import asyncio
4
-
5
3
  from dimine_python_sdk.cli.utils._utils import (
6
4
  get_client,
7
5
  handle_sdk_error,
@@ -9,6 +7,7 @@ from dimine_python_sdk.cli.utils._utils import (
9
7
  output_result,
10
8
  parse_json_arg,
11
9
  require_remote_sdk,
10
+ run_cli_async,
12
11
  )
13
12
 
14
13
 
@@ -35,7 +34,7 @@ def cmd_camera_set(fmt: str, center_point: str, normal_vector: str) -> None:
35
34
  }
36
35
 
37
36
  try:
38
- result = asyncio.run(_run())
37
+ result = run_cli_async(_run())
39
38
  output_result(result, fmt)
40
39
  except Exception as e:
41
40
  handle_sdk_error(e, fmt, "设置相机视角失败")
@@ -56,7 +55,7 @@ def cmd_camera_screenshot(fmt: str) -> None:
56
55
  }
57
56
 
58
57
  try:
59
- result = asyncio.run(_run())
58
+ result = run_cli_async(_run())
60
59
  output_result(result, fmt)
61
60
  except Exception as e:
62
61
  handle_sdk_error(e, fmt, "截图失败")
@@ -1,7 +1,5 @@
1
1
  """实体相关 CLI 命令。"""
2
2
 
3
- import asyncio
4
-
5
3
  from dimine_python_sdk.cli.utils._utils import (
6
4
  ENTITY_TYPE_CN,
7
5
  fmt_selected_item,
@@ -11,6 +9,7 @@ from dimine_python_sdk.cli.utils._utils import (
11
9
  output_result,
12
10
  parse_entity_ids,
13
11
  require_remote_sdk,
12
+ run_cli_async,
14
13
  safe_int,
15
14
  )
16
15
 
@@ -57,7 +56,7 @@ def cmd_get_selected(fmt: str, page_size: int, page: int) -> None:
57
56
  }
58
57
 
59
58
  try:
60
- result = asyncio.run(_run())
59
+ result = run_cli_async(_run())
61
60
  output_result(result, fmt)
62
61
  except Exception as e:
63
62
  handle_sdk_error(e, fmt, "获取选中实体失败")
@@ -100,7 +99,7 @@ def cmd_select(fmt: str, file_id: str | None, ids: str | None, mode: str) -> Non
100
99
  }
101
100
 
102
101
  try:
103
- result = asyncio.run(_run())
102
+ result = run_cli_async(_run())
104
103
  output_result(result, fmt)
105
104
  except Exception as e:
106
105
  handle_sdk_error(e, fmt, "选中操作失败")
@@ -127,7 +126,7 @@ def cmd_delete(fmt: str, file_id: str | None, ids: str) -> None:
127
126
  }
128
127
 
129
128
  try:
130
- result = asyncio.run(_run())
129
+ result = run_cli_async(_run())
131
130
  output_result(result, fmt)
132
131
  except Exception as e:
133
132
  handle_sdk_error(e, fmt, "删除实体失败")
@@ -162,7 +161,7 @@ def _set_visibility(fmt: str, file_id: str | None, raw_ids: str, action: str) ->
162
161
  }
163
162
 
164
163
  try:
165
- result = asyncio.run(_run())
164
+ result = run_cli_async(_run())
166
165
  output_result(result, fmt)
167
166
  except Exception as e:
168
167
  handle_sdk_error(e, fmt, "设置可见性失败")
@@ -1,7 +1,5 @@
1
1
  """要素/特征定义相关 CLI 命令。"""
2
2
 
3
- import asyncio
4
-
5
3
  from dimine_python_sdk.cli.utils._utils import (
6
4
  get_client,
7
5
  handle_sdk_error,
@@ -10,6 +8,7 @@ from dimine_python_sdk.cli.utils._utils import (
10
8
  parse_fields_arg,
11
9
  raise_or_exit,
12
10
  require_remote_sdk,
11
+ run_cli_async,
13
12
  )
14
13
 
15
14
 
@@ -38,7 +37,7 @@ def cmd_feature_list(fmt: str, file_id: str | None) -> None:
38
37
  return {"file_id": file_id, "feature_count": len(result), "features": result}
39
38
 
40
39
  try:
41
- result = asyncio.run(_run())
40
+ result = run_cli_async(_run())
42
41
  output_result(result, fmt)
43
42
  except SystemExit:
44
43
  raise
@@ -70,7 +69,7 @@ def cmd_feature_create(fmt: str, file_id: str, name: str, fields: str) -> None:
70
69
  }
71
70
 
72
71
  try:
73
- result = asyncio.run(_run())
72
+ result = run_cli_async(_run())
74
73
  output_result(result, fmt)
75
74
  except Exception as e:
76
75
  handle_sdk_error(e, fmt, "创建要素失败")
@@ -100,7 +99,7 @@ def cmd_feature_update(fmt: str, file_id: str, name: str, fields: str) -> None:
100
99
  }
101
100
 
102
101
  try:
103
- result = asyncio.run(_run())
102
+ result = run_cli_async(_run())
104
103
  output_result(result, fmt)
105
104
  except Exception as e:
106
105
  handle_sdk_error(e, fmt, "更新要素失败")
@@ -123,7 +122,7 @@ def cmd_feature_delete(fmt: str, file_id: str, name: str) -> None:
123
122
  }
124
123
 
125
124
  try:
126
- result = asyncio.run(_run())
125
+ result = run_cli_async(_run())
127
126
  output_result(result, fmt)
128
127
  except Exception as e:
129
128
  handle_sdk_error(e, fmt, "删除要素失败")
@@ -1,8 +1,9 @@
1
1
  """文件相关 CLI 命令。"""
2
2
 
3
- import asyncio
3
+ import os
4
4
 
5
5
  from dimine_python_sdk.cli.utils._utils import (
6
+ close_client,
6
7
  get_client,
7
8
  handle_sdk_error,
8
9
  open_client,
@@ -11,6 +12,7 @@ from dimine_python_sdk.cli.utils._utils import (
11
12
  raise_or_exit,
12
13
  require_remote_sdk,
13
14
  resolve_path,
15
+ run_cli_async,
14
16
  )
15
17
 
16
18
 
@@ -36,7 +38,7 @@ def cmd_file_tree(fmt: str) -> None:
36
38
  return {"file_count": len(result), "files": result}
37
39
 
38
40
  try:
39
- result = asyncio.run(_run())
41
+ result = run_cli_async(_run())
40
42
  output_result(result, fmt)
41
43
  except Exception as e:
42
44
  handle_sdk_error(e, fmt, "获取文件树失败")
@@ -84,7 +86,7 @@ def cmd_file_save(fmt: str, output_path: str, file_id: str | None) -> None:
84
86
 
85
87
 
86
88
  def cmd_file_open(fmt: str, path: str) -> None:
87
- """在 Dimine 中打开文件(需 Dimine 运行)。"""
89
+ """在 Dimine 中打开文件(需 Dimine 运行)。如果文件已在 Dimine 中打开,先关闭再重新打开。"""
88
90
  err = require_remote_sdk()
89
91
  if err:
90
92
  output_error(err, fmt)
@@ -93,6 +95,16 @@ def cmd_file_open(fmt: str, path: str) -> None:
93
95
 
94
96
  async def _run():
95
97
  client = await get_client()
98
+
99
+ # 检查是否有同名文件已打开,如果有则先关闭
100
+ target_name = path
101
+ opened_files = await client.get_files()
102
+ for f in opened_files:
103
+ if f.name == target_name:
104
+ await client.close_file(f.id)
105
+ break
106
+
107
+ # 打开文件
96
108
  await client.open_file(file_path)
97
109
  return {
98
110
  "success": True,
@@ -101,7 +113,7 @@ def cmd_file_open(fmt: str, path: str) -> None:
101
113
  }
102
114
 
103
115
  try:
104
- result = asyncio.run(_run())
116
+ result = run_cli_async(_run())
105
117
  output_result(result, fmt)
106
118
  except Exception as e:
107
119
  handle_sdk_error(e, fmt, "打开文件失败")
@@ -123,7 +135,7 @@ def cmd_file_close(fmt: str, file_id: str) -> None:
123
135
  }
124
136
 
125
137
  try:
126
- result = asyncio.run(_run())
138
+ result = run_cli_async(_run())
127
139
  output_result(result, fmt)
128
140
  except Exception as e:
129
141
  handle_sdk_error(e, fmt, "关闭文件失败")
@@ -147,7 +159,7 @@ def cmd_file_create(fmt: str, path: str) -> None:
147
159
  }
148
160
 
149
161
  try:
150
- result = asyncio.run(_run())
162
+ result = run_cli_async(_run())
151
163
  output_result(result, fmt)
152
164
  except Exception as e:
153
165
  handle_sdk_error(e, fmt, "创建文件失败")
@@ -180,7 +192,7 @@ def cmd_file_visibility(fmt: str, file_id: str, action: str) -> None:
180
192
  }
181
193
 
182
194
  try:
183
- result = asyncio.run(_run())
195
+ result = run_cli_async(_run())
184
196
  output_result(result, fmt)
185
197
  except Exception as e:
186
198
  handle_sdk_error(e, fmt, "设置文件可见性失败")
@@ -1,6 +1,5 @@
1
1
  """几何体相关 CLI 命令。"""
2
2
 
3
- import asyncio
4
3
  import sys
5
4
 
6
5
  from dimine_python_sdk.cli.utils._utils import (
@@ -13,6 +12,7 @@ from dimine_python_sdk.cli.utils._utils import (
13
12
  parse_color,
14
13
  output_result,
15
14
  require_remote_sdk,
15
+ run_cli_async,
16
16
  )
17
17
 
18
18
 
@@ -366,7 +366,7 @@ def cmd_geometry_get(fmt: str, file_id: str, layer_id: str | None,
366
366
  }
367
367
 
368
368
  try:
369
- result = asyncio.run(_run())
369
+ result = run_cli_async(_run())
370
370
  output_result(result, fmt)
371
371
  except Exception as e:
372
372
  handle_sdk_error(e, fmt, "获取几何体失败")
@@ -423,7 +423,7 @@ def cmd_geometry_create(fmt: str, geometries: str | None = None,
423
423
  }
424
424
 
425
425
  try:
426
- result = asyncio.run(_run())
426
+ result = run_cli_async(_run())
427
427
  output_result(result, fmt)
428
428
  except Exception as e:
429
429
  handle_sdk_error(e, fmt, "创建几何体失败")
@@ -483,7 +483,7 @@ def cmd_geometry_update(fmt: str, geometries: str | None = None,
483
483
  }
484
484
 
485
485
  try:
486
- result = asyncio.run(_run())
486
+ result = run_cli_async(_run())
487
487
  output_result(result, fmt)
488
488
  except Exception as e:
489
489
  handle_sdk_error(e, fmt, "更新几何体失败")
@@ -557,7 +557,7 @@ def cmd_geometry_upsert(fmt: str, geometries: str | None = None,
557
557
  }
558
558
 
559
559
  try:
560
- result = asyncio.run(_run())
560
+ result = run_cli_async(_run())
561
561
  output_result(result, fmt)
562
562
  except Exception as e:
563
563
  handle_sdk_error(e, fmt, "处理几何体失败")
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import asyncio
6
5
  from typing import TYPE_CHECKING
7
6
 
8
7
  if TYPE_CHECKING:
@@ -13,6 +12,7 @@ from dimine_python_sdk.cli.utils._utils import (
13
12
  get_client,
14
13
  output_error,
15
14
  output_result,
15
+ run_cli_async,
16
16
  require_local_sdk,
17
17
  require_remote_sdk,
18
18
  safe_float,
@@ -170,7 +170,7 @@ def cmd_tunnel(fmt: str, section_type: int, width: float, height: float | None,
170
170
  if err:
171
171
  output_error(f"未提供 --center-lines,需要从 Dimine 获取选中线段: {err}", fmt)
172
172
  try:
173
- lines = asyncio.run(_fetch_center_lines_from_selection())
173
+ lines = run_cli_async(_fetch_center_lines_from_selection())
174
174
  except Exception as e:
175
175
  output_error(f"获取选中线段失败: {e}", fmt)
176
176
  else:
@@ -228,7 +228,7 @@ def cmd_tunnel(fmt: str, section_type: int, width: float, height: float | None,
228
228
  }
229
229
 
230
230
  if import_to_dimine:
231
- import_result = asyncio.run(_import_to_dimine(geometries))
231
+ import_result = run_cli_async(_import_to_dimine(geometries))
232
232
  result["imported"] = import_result
233
233
  else:
234
234
  # 仅在明确不导入时返回简化模型摘要,不含原始坐标
@@ -1,7 +1,5 @@
1
1
  """图层相关 CLI 命令。"""
2
2
 
3
- import asyncio
4
-
5
3
  from dimine_python_sdk.cli.utils._utils import (
6
4
  get_client,
7
5
  handle_sdk_error,
@@ -9,6 +7,7 @@ from dimine_python_sdk.cli.utils._utils import (
9
7
  output_result,
10
8
  raise_or_exit,
11
9
  require_remote_sdk,
10
+ run_cli_async,
12
11
  )
13
12
 
14
13
 
@@ -31,7 +30,7 @@ def cmd_layers(fmt: str, file_id: str | None) -> None:
31
30
  return {"file_id": file_id, "layer_count": len(result), "layers": result}
32
31
 
33
32
  try:
34
- result = asyncio.run(_run())
33
+ result = run_cli_async(_run())
35
34
  output_result(result, fmt)
36
35
  except SystemExit:
37
36
  raise
@@ -63,7 +62,7 @@ def cmd_layer_create(fmt: str, name: str, file_id: str | None) -> None:
63
62
  }
64
63
 
65
64
  try:
66
- result = asyncio.run(_run())
65
+ result = run_cli_async(_run())
67
66
  output_result(result, fmt)
68
67
  except SystemExit:
69
68
  raise
@@ -99,7 +98,7 @@ def cmd_layer_visibility(fmt: str, file_id: str, layer_name: str, action: str) -
99
98
  }
100
99
 
101
100
  try:
102
- result = asyncio.run(_run())
101
+ result = run_cli_async(_run())
103
102
  output_result(result, fmt)
104
103
  except Exception as e:
105
104
  handle_sdk_error(e, fmt, "设置图层可见性失败")
@@ -128,7 +127,7 @@ def cmd_layer_delete(fmt: str, name: str, file_id: str | None) -> None:
128
127
  }
129
128
 
130
129
  try:
131
- result = asyncio.run(_run())
130
+ result = run_cli_async(_run())
132
131
  output_result(result, fmt)
133
132
  except SystemExit:
134
133
  raise
@@ -1,7 +1,5 @@
1
1
  """文字标注相关 CLI 命令。"""
2
2
 
3
- import asyncio
4
-
5
3
  from dimine_python_sdk.cli.utils._utils import (
6
4
  get_client,
7
5
  handle_sdk_error,
@@ -11,8 +9,8 @@ from dimine_python_sdk.cli.utils._utils import (
11
9
  parse_json_arg,
12
10
  parse_point3d,
13
11
  require_remote_sdk,
12
+ run_cli_async,
14
13
  )
15
- from dimine_python_sdk.models.types import TextInfo
16
14
 
17
15
 
18
16
  def cmd_text_add(
@@ -37,6 +35,8 @@ def cmd_text_add(
37
35
  text_infos = []
38
36
  if geometries is not None:
39
37
  # 批量模式:从 JSON 解析完整的 TextInfo 列表
38
+ from dimine_python_sdk.models.types import TextInfo
39
+
40
40
  items = parse_json_arg(geometries, "geometries")
41
41
  if not isinstance(items, list):
42
42
  raise ValueError("--geometries 必须是 JSON 数组格式")
@@ -76,7 +76,7 @@ def cmd_text_add(
76
76
  }
77
77
 
78
78
  try:
79
- result = asyncio.run(_run())
79
+ result = run_cli_async(_run())
80
80
  output_result(result, fmt)
81
81
  except Exception as e:
82
82
  handle_sdk_error(e, fmt, "添加文字标注失败")
@@ -8,60 +8,77 @@
8
8
  - JSON/文本输出格式化
9
9
  """
10
10
 
11
- import asyncio
11
+ from __future__ import annotations
12
+
12
13
  import json
13
14
  import os
14
15
  import re
15
16
  import sys
16
- from typing import Any, Optional
17
+ from typing import TYPE_CHECKING, Any, Optional
17
18
 
18
- from dimine_python_sdk.conn.conn_client import ConnClient
19
+ if TYPE_CHECKING:
20
+ from dimine_python_sdk.conn.conn_client import ConnClient
19
21
 
20
22
  # ==============================================================================
21
23
  # SDK 状态检查
22
24
  # ==============================================================================
23
25
 
24
- _SDK_AVAILABLE = False
25
- _local_sdk_available = False
26
+ _SDK_AVAILABLE: bool | None = None # None = 尚未检查
27
+ _local_sdk_available: bool | None = None # None = 尚未检查
26
28
  _import_error: str = ""
27
29
  open_client = None # type: ignore[assignment]
28
30
 
29
- try:
30
- from dimine_python_sdk.conn.conn_client import open_client as _open_client # type: ignore[no-redef]
31
- open_client = _open_client
32
- _SDK_AVAILABLE = True
33
- except ImportError as e:
34
- _import_error = str(e)
35
- except Exception as e:
36
- _import_error = str(e)
37
31
 
38
- try:
39
- from dimine_python_sdk.lib.native._core import Dm # noqa: F401
40
- _local_sdk_available = True
41
- except ImportError:
42
- pass
43
- except Exception:
44
- pass
32
+ def _check_remote_sdk() -> bool:
33
+ """惰性检查远程 SDK 是否可用。首次调用时导入 conn 模块链并缓存结果。"""
34
+ global _SDK_AVAILABLE, _import_error, open_client
35
+ if _SDK_AVAILABLE is not None:
36
+ return _SDK_AVAILABLE
37
+ try:
38
+ from dimine_python_sdk.conn.conn_client import open_client as _open_client # type: ignore[no-redef]
39
+ open_client = _open_client
40
+ _SDK_AVAILABLE = True
41
+ except ImportError as e:
42
+ _import_error = str(e)
43
+ _SDK_AVAILABLE = False
44
+ except Exception as e:
45
+ _import_error = str(e)
46
+ _SDK_AVAILABLE = False
47
+ return _SDK_AVAILABLE
48
+
49
+
50
+ def _check_local_sdk() -> bool:
51
+ """惰性检查本地 SDK 是否可用。首次调用时加载 DmPyBindInterface.pyd 并缓存结果。"""
52
+ global _local_sdk_available
53
+ if _local_sdk_available is not None:
54
+ return _local_sdk_available
55
+ try:
56
+ from dimine_python_sdk.lib.native._core import Dm # noqa: F401
57
+ _local_sdk_available = True
58
+ except (ImportError, Exception):
59
+ _local_sdk_available = False
60
+ return _local_sdk_available
45
61
 
46
62
 
47
63
  def is_remote_sdk_available() -> bool:
48
64
  """返回远程 SDK 是否可用。"""
49
- return _SDK_AVAILABLE
65
+ return _check_remote_sdk()
50
66
 
51
67
 
52
68
  def is_local_sdk_available() -> bool:
53
69
  """返回本地 SDK 是否可用。"""
54
- return _local_sdk_available
70
+ return _check_local_sdk()
55
71
 
56
72
 
57
73
  def get_import_error() -> str:
58
74
  """返回 SDK 导入错误信息。"""
75
+ _check_remote_sdk() # 确保已检查
59
76
  return _import_error
60
77
 
61
78
 
62
79
  def require_remote_sdk() -> str:
63
80
  """返回远程 SDK 不可用时的错误消息(空字符串表示可用)。"""
64
- if _SDK_AVAILABLE:
81
+ if _check_remote_sdk():
65
82
  return ""
66
83
  return (
67
84
  f"Dimine Python SDK 不可用: {_import_error}\n\n"
@@ -75,7 +92,7 @@ def require_remote_sdk() -> str:
75
92
 
76
93
  def require_local_sdk() -> str:
77
94
  """返回本地 SDK 不可用时的错误消息(空字符串表示可用)。"""
78
- if _local_sdk_available:
95
+ if _check_local_sdk():
79
96
  return ""
80
97
  return (
81
98
  f"Dimine Python SDK 本地模块不可用。\n"
@@ -94,12 +111,37 @@ _client_instance: Optional[Any] = None
94
111
  _client_loop_id: Optional[int] = None
95
112
 
96
113
 
114
+ async def _run_with_cleanup(coro):
115
+ """包装协程,确保在事件循环关闭前清理 WebSocket 客户端。
116
+
117
+ 防止 Windows ProactorEventLoop 关闭时因未清理的 IOCP 句柄导致
118
+ 进程退出码变为 0xC0000409 (-1073740791)。
119
+ """
120
+ try:
121
+ return await coro
122
+ finally:
123
+ await close_client()
124
+
125
+
126
+ def run_cli_async(coro):
127
+ """CLI 命令统一入口:运行异步操作并确保客户端清理。
128
+
129
+ 替代裸 asyncio.run(),在所有 CLI 命令的 _run() 之后调用。
130
+ 用法:result = run_cli_async(_run())
131
+ """
132
+ import asyncio
133
+
134
+ return asyncio.run(_run_with_cleanup(coro))
135
+
136
+
97
137
  async def get_client()->ConnClient:
98
138
  """获取或创建共享的 WebSocket 客户端,避免每次操作都建立新连接。
99
139
 
100
140
  如果检测到事件循环已变更(如 asyncio.run() 多次调用场景),
101
141
  自动丢弃旧连接并重新建立,避免跨事件循环使用 WebSocket 导致的 RuntimeError。
102
142
  """
143
+ import asyncio
144
+
103
145
  global _client_ctx, _client_instance, _client_loop_id
104
146
 
105
147
  # 检测事件循环是否变更(asyncio.run() 每次创建新的事件循环)
@@ -1,6 +1,8 @@
1
1
  import os
2
2
  from pathlib import Path
3
3
  import sys
4
+ from typing import Any
5
+
4
6
  from dimine_python_sdk.logger import logger
5
7
 
6
8
  DM_PYD_NAME = "DmPyBindInterface.pyd"
@@ -19,11 +21,24 @@ def set_dimine_home(dimine_home: str):
19
21
  else:
20
22
  raise Exception(f"dimine_home not found: {dimine_home} or DmPyBindInterface.pyd not found in dimine_home")
21
23
 
22
- # SDK 生命周期管理(re-export from native._core)
23
- from dimine_python_sdk.lib.native._core import init_sdk, uninit_sdk, _ensure_initialized # noqa: E402
24
24
 
25
- # 导入 lib 模块时自动执行懒初始化(幂等,重复调用无副作用)
26
- _ensure_initialized()
25
+ _LAZY_EXPORTS: dict[str, Any] = {}
26
+
27
+
28
+ def __getattr__(name: str) -> Any:
29
+ """PEP 562: 惰性加载 init_sdk / uninit_sdk,避免导入 lib 时立即加载 DmPyBindInterface.pyd。"""
30
+ if name in ("init_sdk", "uninit_sdk"):
31
+ if name not in _LAZY_EXPORTS:
32
+ from dimine_python_sdk.lib.native._core import init_sdk, uninit_sdk
33
+ _LAZY_EXPORTS["init_sdk"] = init_sdk
34
+ _LAZY_EXPORTS["uninit_sdk"] = uninit_sdk
35
+ return _LAZY_EXPORTS[name]
36
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
37
+
38
+
39
+ def __dir__() -> list[str]:
40
+ return sorted(set(list(globals()) + __all__ + list(_LAZY_EXPORTS)))
41
+
27
42
 
28
43
  __all__ = [
29
44
  "set_dimine_home",
@@ -6,6 +6,7 @@ Native Layer 公开入口
6
6
  """
7
7
 
8
8
  from dimine_python_sdk.lib.native._core import (
9
+ _ensure_initialized,
9
10
  init_sdk,
10
11
  uninit_sdk,
11
12
  )
@@ -87,6 +88,9 @@ from dimine_python_sdk.lib.native.exploitation import (
87
88
  unite_polydata,
88
89
  )
89
90
 
91
+ # 导入 native 层时自动执行 SDK 初始化(幂等,重复调用无副作用)
92
+ _ensure_initialized()
93
+
90
94
  __all__ = [
91
95
  # SDK 生命周期
92
96
  "init_sdk",