osis-python 0.6.5__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. {osis_python-0.6.5/src/osis_python.egg-info → osis_python-0.7.0}/PKG-INFO +1 -1
  2. {osis_python-0.6.5 → osis_python-0.7.0/src/osis_python.egg-info}/PKG-INFO +1 -1
  3. {osis_python-0.6.5 → osis_python-0.7.0}/src/osis_python.egg-info/SOURCES.txt +2 -0
  4. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/__init__.py +2 -1
  5. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/boundary/manager.py +17 -2
  6. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/__init__.py +2 -0
  7. osis_python-0.7.0/src/pyosis/core/basic_manager.py +179 -0
  8. osis_python-0.7.0/src/pyosis/core/batch.py +173 -0
  9. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/build.py +182 -164
  10. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/client.py +9 -0
  11. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/command.py +17 -11
  12. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/dynamic/manager.py +19 -3
  13. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/element/manager.py +28 -5
  14. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/geometry/manager.py +7 -1
  15. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/live/manager.py +22 -3
  16. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/load/manager.py +27 -4
  17. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/material/manager.py +8 -1
  18. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/node/manager.py +9 -1
  19. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/property/manager.py +28 -4
  20. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/manager.py +8 -1
  21. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/settlement/manager.py +16 -2
  22. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/stability/manager.py +7 -1
  23. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/stage/manager.py +8 -1
  24. osis_python-0.7.0/tests/test_batch.py +231 -0
  25. osis_python-0.6.5/src/pyosis/core/basic_manager.py +0 -19
  26. {osis_python-0.6.5 → osis_python-0.7.0}/LICENSE +0 -0
  27. {osis_python-0.6.5 → osis_python-0.7.0}/README.md +0 -0
  28. {osis_python-0.6.5 → osis_python-0.7.0}/pyproject.toml +0 -0
  29. {osis_python-0.6.5 → osis_python-0.7.0}/setup.cfg +0 -0
  30. {osis_python-0.6.5 → osis_python-0.7.0}/src/osis_python.egg-info/dependency_links.txt +0 -0
  31. {osis_python-0.6.5 → osis_python-0.7.0}/src/osis_python.egg-info/requires.txt +0 -0
  32. {osis_python-0.6.5 → osis_python-0.7.0}/src/osis_python.egg-info/top_level.txt +0 -0
  33. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/boundary/__init__.py +0 -0
  34. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/boundary/interface.py +0 -0
  35. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/control/__init__.py +0 -0
  36. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/control/control.py +0 -0
  37. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/control/io.py +0 -0
  38. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/control/manager.py +0 -0
  39. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/control/nonlinear.py +0 -0
  40. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/all_func.py +0 -0
  41. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/apdl_sync.py +0 -0
  42. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/engine.py +0 -0
  43. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/exceptions.py +0 -0
  44. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/references.py +0 -0
  45. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/solver.py +0 -0
  46. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/core/sync_apdl_cli.py +0 -0
  47. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/display/__init__.py +0 -0
  48. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/display/interface.py +0 -0
  49. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/display/manager.py +0 -0
  50. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/dynamic/__init__.py +0 -0
  51. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/dynamic/load_to_mass.py +0 -0
  52. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/dynamic/modal.py +0 -0
  53. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/dynamic/seismic.py +0 -0
  54. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/element/__init__.py +0 -0
  55. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/element/interface.py +0 -0
  56. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/general/__init__.py +0 -0
  57. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/general/interface.py +0 -0
  58. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/geometry/__init__.py +0 -0
  59. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/geometry/interface.py +0 -0
  60. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/live/__init__.py +0 -0
  61. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/live/analysis.py +0 -0
  62. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/live/grade.py +0 -0
  63. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/live/lane.py +0 -0
  64. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/load/__init__.py +0 -0
  65. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/load/loadcase.py +0 -0
  66. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/load/static.py +0 -0
  67. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/load/tendon.py +0 -0
  68. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/material/__init__.py +0 -0
  69. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/material/interface.py +0 -0
  70. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/node/__init__.py +0 -0
  71. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/node/interface.py +0 -0
  72. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/post/__init__.py +0 -0
  73. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/post/combine.py +0 -0
  74. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/post/design_check.py +0 -0
  75. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/post/display.py +0 -0
  76. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/post/manager.py +0 -0
  77. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/project/__init__.py +0 -0
  78. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/project/interface.py +0 -0
  79. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/project/manager.py +0 -0
  80. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/property/__init__.py +0 -0
  81. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/property/component_thickness.py +0 -0
  82. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/property/coordinate.py +0 -0
  83. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/property/creep_shrink.py +0 -0
  84. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/property/damping.py +0 -0
  85. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/property/pu_curve.py +0 -0
  86. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/quick_building/__init__.py +0 -0
  87. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/quick_building/interface.py +0 -0
  88. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/result/__init__.py +0 -0
  89. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/result/check.py +0 -0
  90. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/result/env.py +0 -0
  91. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/result/loadcase.py +0 -0
  92. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/result/manager.py +0 -0
  93. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/result/result.py +0 -0
  94. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/__init__.py +0 -0
  95. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/common.py +0 -0
  96. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/composite.py +0 -0
  97. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/numeric.py +0 -0
  98. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/param.py +0 -0
  99. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/rebar.py +0 -0
  100. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/rib.py +0 -0
  101. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/section/steel.py +0 -0
  102. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/settlement/__init__.py +0 -0
  103. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/settlement/interface.py +0 -0
  104. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/stability/__init__.py +0 -0
  105. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/stability/buckling.py +0 -0
  106. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/stage/__init__.py +0 -0
  107. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/stage/define.py +0 -0
  108. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/stage/overall.py +0 -0
  109. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/thickness/__init__.py +0 -0
  110. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/thickness/interface.py +0 -0
  111. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/thickness/manager.py +0 -0
  112. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/transfer/__init__.py +0 -0
  113. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/transfer/generator.py +0 -0
  114. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/transfer/matrix.py +0 -0
  115. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/transfer/out_to_python.py +0 -0
  116. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/transfer/parser.py +0 -0
  117. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/transfer/routes.py +0 -0
  118. {osis_python-0.6.5 → osis_python-0.7.0}/src/pyosis/transfer/split.py +0 -0
  119. {osis_python-0.6.5 → osis_python-0.7.0}/tests/test_delete_occupied.py +0 -0
  120. {osis_python-0.6.5 → osis_python-0.7.0}/tests/test_project_manager.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osis-python
3
- Version: 0.6.5
3
+ Version: 0.7.0
4
4
  Summary: A Python client library for Osis APDL
5
5
  Author-email: "CCCC Highway Consultant Co. Ltd." <support@osisbim.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: osis-python
3
- Version: 0.6.5
3
+ Version: 0.7.0
4
4
  Summary: A Python client library for Osis APDL
5
5
  Author-email: "CCCC Highway Consultant Co. Ltd." <support@osisbim.com>
6
6
  License-Expression: MIT
@@ -19,6 +19,7 @@ src/pyosis/core/__init__.py
19
19
  src/pyosis/core/all_func.py
20
20
  src/pyosis/core/apdl_sync.py
21
21
  src/pyosis/core/basic_manager.py
22
+ src/pyosis/core/batch.py
22
23
  src/pyosis/core/build.py
23
24
  src/pyosis/core/client.py
24
25
  src/pyosis/core/command.py
@@ -111,5 +112,6 @@ src/pyosis/transfer/out_to_python.py
111
112
  src/pyosis/transfer/parser.py
112
113
  src/pyosis/transfer/routes.py
113
114
  src/pyosis/transfer/split.py
115
+ tests/test_batch.py
114
116
  tests/test_delete_occupied.py
115
117
  tests/test_project_manager.py
@@ -7,10 +7,11 @@ A library for extending OSIS functionality.
7
7
  A library that allows users to use CAE with great flexibility.
8
8
  """
9
9
 
10
- __version__ = "0.6.5"
10
+ __version__ = "0.7.0"
11
11
  __author__ = "CCCC Highway Consultant Co. Ltd." # 中交公路规划设计院 - 工程技术研究中心 - lzh - wjh - myf
12
12
 
13
13
  from .core.engine import OSISEngine
14
+ from .core.batch import batch, flush, batch_state, BatchError
14
15
  # from .core import engine
15
16
  # from .ai.agents import BaseAgent
16
17
  # from .core import osis_run
@@ -24,6 +24,8 @@ from enum import IntEnum
24
24
 
25
25
  from ..core.client import osis_client
26
26
  from ..core import get_references, raise_if_occupied
27
+ from ..core.basic_manager import BasicManager
28
+ from ..core.batch import batch_state
27
29
  from .interface import (
28
30
  osis_boundary_general,
29
31
  osis_boundary_elstcspt,
@@ -386,6 +388,8 @@ class BoundaryGroup:
386
388
 
387
389
  def refresh(self) -> BoundaryGroup:
388
390
  """刷新当前边界组对象并同步到对象属性"""
391
+ if batch_state.active:
392
+ return self
389
393
  resp = osis_client("GetBoundaryGroupInfoByNames", {"name": [self.name]})
390
394
  if not resp['success']:
391
395
  raise RuntimeError(f"刷新边界组 {self.name} 失败: {resp['error']}")
@@ -476,7 +480,7 @@ class BoundaryGroup:
476
480
  # ──────────────────────────────────────────────
477
481
 
478
482
 
479
- class BoundaryGroupManager:
483
+ class BoundaryGroupManager(BasicManager):
480
484
  """边界组管理器
481
485
 
482
486
  统一管理边界组的创建、删除和查询。组成员操作在 BoundaryGroup 对象上进行。
@@ -492,6 +496,7 @@ class BoundaryGroupManager:
492
496
  >>> bg.remove(1)
493
497
  >>> bg.replace("3by4") # 组内把边界 3 替换为 4
494
498
  """
499
+ _entity_class = BoundaryGroup
495
500
 
496
501
  def __init__(self) -> None:
497
502
  ...
@@ -581,6 +586,10 @@ class BoundaryGroupManager:
581
586
  if not isinstance(names, list):
582
587
  raise TypeError(f"不支持的名称类型: {type(name)}")
583
588
 
589
+ # batch 模式:返回延迟对象(零查询),访问真实属性时才物化
590
+ lazy = self._batch_lazy(names)
591
+ if lazy is not None:
592
+ return lazy
584
593
  resp = osis_client("GetBoundaryGroupInfoByNames", {"name": names})
585
594
  if not resp['success']:
586
595
  raise RuntimeError(f"{resp['error']}")
@@ -620,7 +629,7 @@ class BoundaryGroupManager:
620
629
  # ──────────────────────────────────────────────
621
630
 
622
631
 
623
- class BoundaryManager:
632
+ class BoundaryManager(BasicManager):
624
633
  """边界管理器
625
634
 
626
635
  统一管理边界的创建、删除、修改和查询。
@@ -635,6 +644,8 @@ class BoundaryManager:
635
644
  >>> bg.add(1, 2)
636
645
  >>> bg = boundary_manager.group.get("桥台1")
637
646
  """
647
+ _entity_class = Boundary
648
+ _entity_key_attr = "no"
638
649
 
639
650
  def __init__(self) -> None:
640
651
  self._group_manager = BoundaryGroupManager()
@@ -1029,6 +1040,10 @@ class BoundaryManager:
1029
1040
  elif not isinstance(no, list):
1030
1041
  raise TypeError(f"不支持的编号类型: {type(no)}")
1031
1042
 
1043
+ # batch 模式:返回延迟对象(零查询),访问真实属性时才物化
1044
+ lazy = self._batch_lazy(no)
1045
+ if lazy is not None:
1046
+ return lazy
1032
1047
  resp = osis_client("GetBoundaryInfoByNos", {"no": no})
1033
1048
  if not resp['success']:
1034
1049
  raise RuntimeError(f"{resp['error']}")
@@ -10,5 +10,7 @@ OSISEngine中的函数,将会分发到各个模块,被再次封装,意图
10
10
 
11
11
  from .command import REGISTRY, osis_run, set_run_mode
12
12
  from .client import set_osis_port, set_osis_url
13
+ from .batch import batch, flush, batch_state, BatchError
14
+ from .basic_manager import BasicManager, LazyRef
13
15
  from .exceptions import DependencyError
14
16
  from .references import get_references, raise_if_occupied
@@ -0,0 +1,179 @@
1
+ """manager 基类:batch 模式的延迟对象(LazyRef)与编号记账基础设施
2
+
3
+ batch 模式下的 get() 语义:
4
+ - 返回 LazyRef 延迟对象,零服务器交互
5
+ - 身份属性 name / no 直接返回主键(链式调用 engine.XXX.get(1).create_XXX
6
+ 只需要身份,全程不查询)
7
+ - 访问其他任意属性/方法时才"物化":先自动冲刷缓冲,再查询服务器取回
8
+ 真实对象,之后完全代理真实对象(每个 LazyRef 只物化一次)
9
+
10
+ manager 接入方式(每个 manager 两处小改动,与具体命令无关):
11
+ class NodeManager(BasicManager):
12
+ def _next_no(self) -> int:
13
+ return self._batched_next_no( # 1) 自动编号批内记账
14
+ lambda: max((n.no for n in self._load()), default=0))
15
+
16
+ def get(self, no):
17
+ ...编号归一化与类型检查...
18
+ lazy = self._batch_lazy(no) # 2) batch 模式返回 LazyRef
19
+ if lazy is not None:
20
+ return lazy
21
+ ...原有服务器查询逻辑...
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import functools
27
+ from contextlib import contextmanager
28
+ from typing import Any
29
+
30
+ from .batch import batch_state
31
+
32
+
33
+ class LazyRef:
34
+ """batch 模式下 get() 的延迟对象
35
+
36
+ 由 BasicManager._batch_lazy 创建,用户不应直接实例化。
37
+ - 身份属性(name/no)零查询直通
38
+ - 方法访问 → 绑定到"幽灵实例"(类型正确、仅注入身份字段),
39
+ 调用即排队命令,零服务器交互——链式调用 get().create_xxx() 的关键
40
+ - 数据字段访问 → 物化(冲刷 + 服务器查询)后返回真实值
41
+ """
42
+
43
+ __slots__ = ("_manager", "_key", "_real", "_ghost")
44
+
45
+ def __init__(self, manager: "BasicManager", key: Any) -> None:
46
+ object.__setattr__(self, "_manager", manager)
47
+ object.__setattr__(self, "_key", key)
48
+ object.__setattr__(self, "_real", None)
49
+ object.__setattr__(self, "_ghost", None)
50
+
51
+ # ── 身份属性:零查询直通 ──────────────────
52
+
53
+ @property
54
+ def name(self) -> Any:
55
+ """对象名称(即 get 的主键),物化后返回真实值"""
56
+ return self._key if self._real is None else self._real.name
57
+
58
+ @property
59
+ def no(self) -> Any:
60
+ """对象编号(即 get 的主键),物化后返回真实值"""
61
+ return self._key if self._real is None else self._real.no
62
+
63
+ # ── 幽灵实例与物化 ────────────────────────
64
+
65
+ def _ghost_method(self, attr: str):
66
+ """方法访问:绑定到幽灵实例(真类型 + 身份字段),调用即排队"""
67
+ if self._ghost is None:
68
+ cls = self._ghost_class()
69
+ ghost = object.__new__(cls) # 不走 __init__(缺服务器数据)
70
+ # 身份字段双保险(object.__setattr__ 兼容 frozen dataclass)
71
+ object.__setattr__(ghost, "name", self._key)
72
+ object.__setattr__(ghost, "no", self._key)
73
+ object.__setattr__(self, "_ghost", ghost)
74
+ return getattr(self._ghost, attr)
75
+
76
+ def _ghost_class(self) -> type:
77
+ m = self._manager
78
+ return getattr(type(m), "_entity_class", None) or type(m).get.__globals__.get(
79
+ type(m).__name__.replace("Manager", ""), object
80
+ )
81
+
82
+ def _materialize(self) -> Any:
83
+ """冲刷缓冲并从服务器取回真实对象(每个 LazyRef 只执行一次)"""
84
+ if self._real is None:
85
+ with self._manager._no_lazy():
86
+ real = self._manager.get(self._key)
87
+ if real is None:
88
+ raise RuntimeError(
89
+ f"batch 延迟对象物化失败:{type(self._manager).__name__} 中不存在 {self._key!r}"
90
+ )
91
+ object.__setattr__(self, "_real", real)
92
+ return self._real
93
+
94
+ def __getattr__(self, attr: str) -> Any:
95
+ # 方法 → 幽灵实例(零查询,链式调用直接排队)
96
+ cls = getattr(type(self._manager), "_entity_class", None)
97
+ if cls is not None and callable(getattr(cls, attr, None)):
98
+ return self._ghost_method(attr)
99
+ # 数据字段 → 物化后取真实值
100
+ return getattr(self._materialize(), attr)
101
+
102
+ def __repr__(self) -> str:
103
+ if self._real is not None:
104
+ return repr(self._real)
105
+ return f"LazyRef({type(self._manager).__name__}, {self._key!r})"
106
+
107
+
108
+ class BasicManager:
109
+ """所有 manager 的基类(batch 延迟对象 + 自动编号守卫)
110
+
111
+ 子类可声明(用于 LazyRef 的链式调用优化,方法访问零查询直通排队):
112
+ - _entity_class: 该 manager 管理的数据类(如 ElementGroup)
113
+ - _entity_key_attr: 身份字段名("name" 或 "no",默认 "name")
114
+ """
115
+
116
+ _entity_class: type | None = None
117
+ _entity_key_attr: str = "name"
118
+ _no_lazy_flag: bool = False
119
+
120
+ def __init_subclass__(cls, **kwargs) -> None:
121
+ super().__init_subclass__(**kwargs)
122
+ # 自动包装子类定义的 _next_no:batch 模式下直接报错(自动编号需要
123
+ # 拉全量查询,会打断批量;批内请显式传编号)。子类无需任何配合代码。
124
+ orig = cls.__dict__.get("_next_no")
125
+ if orig is not None and not getattr(orig, "_batch_guarded", False):
126
+ @functools.wraps(orig)
127
+ def _next_no(self, *args, **kw):
128
+ if batch_state.active:
129
+ raise RuntimeError(
130
+ "batch 模式下不支持自动编号(查询最大编号会打断批量):"
131
+ "请显式传入编号,或退出 batch 后再使用自动编号"
132
+ )
133
+ return orig(self, *args, **kw)
134
+
135
+ _next_no._batch_guarded = True
136
+ cls._next_no = _next_no
137
+
138
+ # ── 原有骨架保留(兼容旧引用) ─────────────
139
+
140
+ def create(self, *args, **kwargs):
141
+ pass
142
+
143
+ def get(self, *args, **kwargs):
144
+ pass
145
+
146
+ def all(self, *args, **kwargs):
147
+ pass
148
+
149
+ def delete(self, *args, **kwargs):
150
+ pass
151
+
152
+ def clear(self, *args, **kwargs):
153
+ pass
154
+
155
+ # ── 子类工具方法 ──────────────────────────
156
+
157
+ def _batch_lazy(self, keys: list) -> Any:
158
+ """batch 模式下为 get() 构造延迟对象
159
+
160
+ Args:
161
+ keys: 已归一化的主键列表(调用方完成类型检查与归一化后传入)
162
+
163
+ Returns:
164
+ 单个 LazyRef(keys 长度为 1)或 LazyRef 列表;
165
+ 非 batch 模式(或物化过程中)返回 None,调用方走原有查询逻辑
166
+ """
167
+ if not batch_state.active or self._no_lazy_flag or not keys:
168
+ return None
169
+ refs = [LazyRef(self, k) for k in keys]
170
+ return refs[0] if len(refs) == 1 else refs
171
+
172
+ @contextmanager
173
+ def _no_lazy(self):
174
+ """临时关闭延迟语义(LazyRef 物化时回查服务器用)"""
175
+ self._no_lazy_flag = True
176
+ try:
177
+ yield
178
+ finally:
179
+ self._no_lazy_flag = False
@@ -0,0 +1,173 @@
1
+ """命令批量执行(batch)核心
2
+
3
+ 设计要点:
4
+ - 任何经 REGISTRY 注册的命令函数在 batch 激活时都只进本地缓冲,退出 with 时
5
+ 拼成一条命令流、一次 /OSIS_Run 发送(服务端 SplitOsisCommands + 单次 Execution)
6
+ - 查询类调用(osis_client 的所有接口,含用户直接调用的 OSIS_Run)会先自动冲刷
7
+ 缓冲,保证查询永远看到"已执行完毕"的状态
8
+ - manager 通过影子工厂表(BasicManager._shadow_factories)在命令入缓冲时登记
9
+ 影子对象,使 create 尾部的回查 get() 命中本地影子而不触发冲刷
10
+ - 错误策略:冲刷失败抛 BatchError(附 OSIS 原文);不重试、不退避、默认不二分
11
+ (isolate=True 可选二分定位坏命令,仅供调试,无 sleep)
12
+
13
+ 用法:
14
+ >>> import pyosis
15
+ >>> with pyosis.batch():
16
+ ... for i in range(1000):
17
+ ... engine.element.create_beam3d(None, i, i + 1, 1, 1, 1)
18
+ ... # 退出 with 时 1000 条命令一次发送
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ from contextlib import contextmanager
24
+ from typing import Any, Callable, Iterator
25
+
26
+ from .client import osis_client
27
+
28
+
29
+ class BatchError(RuntimeError):
30
+ """批量冲刷失败
31
+
32
+ Attributes:
33
+ error: OSIS 返回的错误原文
34
+ failed: 定位到的失败命令列表(isolate 模式下逐条列出,否则为空)
35
+ executed: 冲刷前缓冲中的命令总数
36
+ """
37
+
38
+ def __init__(self, error: str, failed: list[str] | None = None, executed: int = 0):
39
+ self.error = error
40
+ self.failed = failed or []
41
+ self.executed = executed
42
+ msg = f"批量冲刷失败(共 {executed} 条命令): {error}"
43
+ if self.failed:
44
+ lines = "\n".join(f" [FAIL] {c}" for c in self.failed)
45
+ msg += f"\n失败命令({len(self.failed)} 条):\n{lines}"
46
+ super().__init__(msg)
47
+
48
+
49
+ class _BatchState:
50
+ """batch 全局状态(模块级单例 batch_state)"""
51
+
52
+ def __init__(self) -> None:
53
+ self.buffer: list[str] = [] # 已入队的命令流(每条自带分号终止符)
54
+ self.depth: int = 0 # batch 嵌套深度,>0 表示激活
55
+ self.flushing: bool = False # 冲刷进行中(防止拦截器递归冲刷)
56
+
57
+ @property
58
+ def active(self) -> bool:
59
+ """batch 模式是否激活"""
60
+ return self.depth > 0
61
+
62
+
63
+ batch_state = _BatchState()
64
+
65
+
66
+ # ──────────────────────────────────────────────
67
+ # 冲刷
68
+ # ──────────────────────────────────────────────
69
+
70
+ def flush(
71
+ isolate: bool = False,
72
+ timeout: float | None = None,
73
+ max_size: int | None = None,
74
+ ) -> int:
75
+ '''冲刷缓冲:把已入队的全部命令拼成一条流,一次 /OSIS_Run 发送
76
+
77
+ 一般无需手动调用——退出 batch 上下文或任何查询调用都会自动冲刷。
78
+
79
+ Args:
80
+ isolate (bool): 冲刷失败时是否二分定位失败命令(仅供调试,
81
+ 无重试无退避;二分重放假设失败命令本身无副作用残留)
82
+ timeout (float): HTTP 超时秒数,默认 600(大批量 Execution 耗时较长)
83
+ max_size (int): 单次请求最大命令条数,默认不限(一次全发)。
84
+ 超大模型可设 5000 之类的安全阀,将分多次请求发送
85
+
86
+ Returns:
87
+ int: 本次冲刷发送的命令条数
88
+
89
+ Raises:
90
+ BatchError: OSIS 返回失败时抛出(isolate=True 时附失败命令清单)
91
+ '''
92
+ if batch_state.flushing or not batch_state.buffer:
93
+ return 0
94
+ cmds = batch_state.buffer[:]
95
+ batch_state.buffer.clear()
96
+ batch_state.flushing = True
97
+ try:
98
+ chunks = (
99
+ [cmds[i:i + max_size] for i in range(0, len(cmds), max_size)]
100
+ if max_size else [cmds]
101
+ )
102
+ failed: list[str] = []
103
+ for chunk in chunks:
104
+ failed += _send(chunk, isolate=isolate, timeout=timeout)
105
+ if failed:
106
+ raise BatchError(
107
+ failed_error(failed),
108
+ failed=[c.split("||", 1)[1] for c in failed],
109
+ executed=len(cmds),
110
+ )
111
+ return len(cmds)
112
+ finally:
113
+ batch_state.flushing = False
114
+
115
+
116
+ def failed_error(failed: list[str]) -> str:
117
+ """汇总失败命令对应的 OSIS 错误信息(_send 记录在命令前缀)"""
118
+ errs = {c.split("||", 1)[0] for c in failed if "||" in c}
119
+ return "; ".join(errs) or "未知错误"
120
+
121
+
122
+ def _send(cmds: list[str], isolate: bool = False, timeout: float | None = None) -> list[str]:
123
+ """发送一组命令,返回失败命令列表("错误||命令" 格式;成功返回空表)"""
124
+ stream = "".join(cmds)
125
+ resp = osis_client(
126
+ "OSIS_Run",
127
+ {"strCmd": stream, "mode": "exec"},
128
+ timeout=timeout if timeout is not None else 600,
129
+ )
130
+ if resp.get("success"):
131
+ return []
132
+ err = resp.get("error", "")
133
+ if not isolate or len(cmds) == 1:
134
+ return [f"{err}||{cmds[0]}"] if len(cmds) == 1 else [f"{err}||(共{len(cmds)}条)"]
135
+ # 二分定位:无重试无退避,把坏命令找出来
136
+ mid = len(cmds) // 2
137
+ return _send(cmds[:mid], True, timeout) + _send(cmds[mid:], True, timeout)
138
+
139
+
140
+ # ──────────────────────────────────────────────
141
+ # batch 上下文
142
+ # ──────────────────────────────────────────────
143
+
144
+ @contextmanager
145
+ def batch() -> Iterator[None]:
146
+ '''batch 模式上下文:内部所有命令只进缓冲,退出时一次性发送
147
+
148
+ >>> with pyosis.batch():
149
+ ... engine.node.create(None, 0, 0, 0)
150
+ ... engine.node.create(None, 1, 0, 0)
151
+ ... element_manager.create_beam3d(None, 1, 2, 1, 1, 1)
152
+ ... # 退出时 3 条命令拼成一条流,一次 HTTP 请求
153
+
154
+ 规则:
155
+ * 正常退出:自动 flush(),缓冲内全部命令一次执行
156
+ * 异常退出:丢弃本次进入后新增的缓冲命令并抛出原异常
157
+ (半截模型不落盘;需要保留请自行 try/finally + flush())
158
+ * 支持嵌套:内层退出不冲刷,最外层退出才统一冲刷
159
+ * 期间任何查询(get/all/solve/用户直接 osis_run 等)会先自动
160
+ 冲刷缓冲再执行,保证查询结果实时
161
+ '''
162
+ batch_state.depth += 1
163
+ added = len(batch_state.buffer)
164
+ try:
165
+ yield
166
+ except BaseException:
167
+ del batch_state.buffer[added:]
168
+ batch_state.depth -= 1
169
+ raise
170
+ else:
171
+ batch_state.depth -= 1
172
+ if batch_state.depth == 0:
173
+ flush()