ipa-python-kit 0.1.3.dev1__py3-none-any.whl → 1.0.1__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.
- ipa/app/__init__.py +57 -0
- ipa/data_type/unitable.py +3 -3
- ipa/datetime/__init__.py +27 -0
- ipa/datetime/core.py +7 -7
- ipa/func/dict.py +15 -6
- ipa/network/__init__.py +23 -3
- ipa/pkg/__init__.py +80 -24
- ipa/pkg/hot_reload.py +4 -2
- ipa/pydantic/core/__init__.py +1 -0
- ipa/version.py +8 -18
- ipa/websocket/core/__init__.py +10 -7
- {ipa_python_kit-0.1.3.dev1.dist-info → ipa_python_kit-1.0.1.dist-info}/METADATA +20 -8
- {ipa_python_kit-0.1.3.dev1.dist-info → ipa_python_kit-1.0.1.dist-info}/RECORD +22 -24
- {ipa_python_kit-0.1.3.dev1.dist-info → ipa_python_kit-1.0.1.dist-info}/WHEEL +1 -1
- ipa/extra/simpy/__init__.py +0 -1
- ipa/extra/simpy/core.py +0 -288
- ipa/func/app.py +0 -12
- /ipa/{logger → logging}/__init__.py +0 -0
- /ipa/{logger → logging}/builder.py +0 -0
- /ipa/{logger → logging}/fmt.py +0 -0
- /ipa/{logger → logging}/handler.py +0 -0
- /ipa/{logger → logging}/model/__init__.py +0 -0
- /ipa/{logger → logging}/model/dict_config.py +0 -0
- /ipa/{logger → logging}/util.py +0 -0
- {ipa_python_kit-0.1.3.dev1.dist-info → ipa_python_kit-1.0.1.dist-info}/top_level.txt +0 -0
ipa/app/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
__doc__ = """
|
|
2
|
+
配置模块
|
|
3
|
+
包含一些全局配置
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class IAppConfig(ABC):
|
|
14
|
+
"""
|
|
15
|
+
应用配置接口
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def get_runtime_root_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def get_root_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
23
|
+
"""
|
|
24
|
+
此方法只是一个别名,返回与get_runtime_root_dir相同的结果
|
|
25
|
+
"""
|
|
26
|
+
return self.get_runtime_root_dir(*args, **kwargs)
|
|
27
|
+
|
|
28
|
+
# @abstractmethod
|
|
29
|
+
# def get_user_data_root_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
30
|
+
# pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def get_logs_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def get_config_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def get_data_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
def get_temp_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
45
|
+
raise NotImplementedError()
|
|
46
|
+
|
|
47
|
+
def get_middleware_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
48
|
+
raise NotImplementedError()
|
|
49
|
+
|
|
50
|
+
def get_extension_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
51
|
+
raise NotImplementedError()
|
|
52
|
+
|
|
53
|
+
def get_app_logger(self, *args, **kwargs) -> logging.Logger:
|
|
54
|
+
raise NotImplementedError()
|
|
55
|
+
|
|
56
|
+
def get_sidecar_dir(self, *args, **kwargs) -> Optional[Path]:
|
|
57
|
+
raise NotImplementedError()
|
ipa/data_type/unitable.py
CHANGED
|
@@ -196,15 +196,15 @@ class Time(QuantifiedValue):
|
|
|
196
196
|
pass
|
|
197
197
|
|
|
198
198
|
|
|
199
|
-
def get_time(
|
|
199
|
+
def get_time(length: Length, v: Speed):
|
|
200
200
|
"""
|
|
201
201
|
距离/速度=时间
|
|
202
202
|
TODO:处理单位
|
|
203
203
|
"""
|
|
204
|
-
assert
|
|
204
|
+
assert length.value is not None, "length is not specified"
|
|
205
205
|
assert v.value is not None, "speed is not specified"
|
|
206
206
|
assert v.value != 0, "speed cannot be zero"
|
|
207
|
-
return abs(
|
|
207
|
+
return abs(length.value / v.value)
|
|
208
208
|
|
|
209
209
|
|
|
210
210
|
def get_time_3d(delta_distance: Point, v: ThreeDimensionalVelocity):
|
ipa/datetime/__init__.py
CHANGED
|
@@ -1 +1,28 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
1
4
|
from .core import *
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def timestamp(
|
|
8
|
+
unit: Literal["second", "millisecond", "microsecond"] = "millisecond",
|
|
9
|
+
type: Union[float, int] = int,
|
|
10
|
+
) -> Union[float, int]:
|
|
11
|
+
"""
|
|
12
|
+
获取当前时间戳
|
|
13
|
+
Args:
|
|
14
|
+
unit: 时间戳单位,默认毫秒
|
|
15
|
+
type: 时间戳类型,默认整数
|
|
16
|
+
Returns:
|
|
17
|
+
时间戳
|
|
18
|
+
"""
|
|
19
|
+
s = time.time()
|
|
20
|
+
if unit == "millisecond":
|
|
21
|
+
v = s * 1000
|
|
22
|
+
elif unit == "microsecond":
|
|
23
|
+
v = s * 1000000
|
|
24
|
+
elif unit == "second":
|
|
25
|
+
v = s
|
|
26
|
+
else:
|
|
27
|
+
raise NotImplementedError(f"unsupported unit: {unit}")
|
|
28
|
+
return type(v)
|
ipa/datetime/core.py
CHANGED
|
@@ -2,11 +2,6 @@ from datetime import datetime
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Union
|
|
4
4
|
|
|
5
|
-
try:
|
|
6
|
-
import pandas
|
|
7
|
-
except ImportError:
|
|
8
|
-
pandas = None
|
|
9
|
-
|
|
10
5
|
from dateutil.parser import parse
|
|
11
6
|
|
|
12
7
|
|
|
@@ -35,8 +30,13 @@ def parse_datetime(date_str: str):
|
|
|
35
30
|
if not date_str: # 处理空值
|
|
36
31
|
return None
|
|
37
32
|
|
|
38
|
-
|
|
39
|
-
|
|
33
|
+
try:
|
|
34
|
+
import pandas as pd
|
|
35
|
+
|
|
36
|
+
if isinstance(date_str, (pd.Timestamp)):
|
|
37
|
+
return date_str.to_pydatetime()
|
|
38
|
+
except ImportError:
|
|
39
|
+
pass
|
|
40
40
|
|
|
41
41
|
if isinstance(date_str, (datetime)):
|
|
42
42
|
return date_str
|
ipa/func/dict.py
CHANGED
|
@@ -2,7 +2,7 @@ __doc__ = """
|
|
|
2
2
|
字典工具类
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
from typing import Any,
|
|
5
|
+
from typing import Any, Dict, Iterable, Optional, Tuple, TypeVar, Union
|
|
6
6
|
|
|
7
7
|
cn_en_dict = dict(
|
|
8
8
|
半径="radius",
|
|
@@ -32,24 +32,33 @@ def remove_key_not_in(dc: dict, keys: Iterable[Any], delete_value: bool = False)
|
|
|
32
32
|
Args:
|
|
33
33
|
delete_value: 是否同时del value
|
|
34
34
|
"""
|
|
35
|
+
keys_removed = []
|
|
35
36
|
for key in list(dc.keys()):
|
|
36
37
|
if key not in keys:
|
|
37
38
|
v = dc.pop(key, None)
|
|
38
39
|
if delete_value and v is not None:
|
|
39
40
|
del v
|
|
41
|
+
keys_removed.append(key)
|
|
42
|
+
return keys_removed
|
|
40
43
|
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
K = TypeVar("K")
|
|
46
|
+
V = TypeVar("V")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_the_first_existent_key(
|
|
50
|
+
obj: Dict[K, V], *keys: K, default: Optional[V] = None
|
|
51
|
+
) -> Tuple[Optional[K], Optional[V]]:
|
|
43
52
|
"""
|
|
44
53
|
获取第一个存在的key的值
|
|
45
54
|
|
|
46
55
|
Args:
|
|
47
|
-
obj (
|
|
48
|
-
keys (Iterable[
|
|
49
|
-
default (
|
|
56
|
+
obj (Dict[K,V]): 输入的字典,键名可能是中文或其他形式。
|
|
57
|
+
keys (Iterable[K]): 要查找的键名列表。
|
|
58
|
+
default (V, optional): 如果没有找到任何键,返回的默认值。默认值为None。
|
|
50
59
|
|
|
51
60
|
Returns:
|
|
52
|
-
(Tuple[
|
|
61
|
+
(Tuple[K, V]): 第一个存在的键值对,键为存在的键名,值为对应的值。如果没有找到任何键,返回(None, default)。
|
|
53
62
|
"""
|
|
54
63
|
for key in keys:
|
|
55
64
|
if key in obj:
|
ipa/network/__init__.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from typing import Any, Dict, List, Literal, Optional, Union
|
|
2
2
|
|
|
3
|
-
from
|
|
3
|
+
from ipa.data_type import Point
|
|
4
4
|
|
|
5
5
|
Grid2 = List[List[Point]]
|
|
6
6
|
Grid3 = List[Grid2]
|
|
7
7
|
|
|
8
|
+
from ipa.func import get_the_first_existent_key
|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
def to_grid(
|
|
10
12
|
points: List[Point], include_x=True, include_y=True, include_z=False, **kwargs
|
|
@@ -133,7 +135,20 @@ def nx_digraph_to_graph(
|
|
|
133
135
|
|
|
134
136
|
links = []
|
|
135
137
|
for u, v, attrs in G.edges(data=True):
|
|
136
|
-
|
|
138
|
+
_, label = get_the_first_existent_key(attrs, "label", "name", "id")
|
|
139
|
+
links.append(
|
|
140
|
+
opts.GraphLink(
|
|
141
|
+
source=str(u),
|
|
142
|
+
target=str(v),
|
|
143
|
+
label_opts=opts.LabelOpts(
|
|
144
|
+
is_show=False, position="middle", formatter=label, font_size=15
|
|
145
|
+
),
|
|
146
|
+
emphasis_label_opts=opts.LabelOpts(
|
|
147
|
+
is_show=True,
|
|
148
|
+
),
|
|
149
|
+
linestyle_opts=opts.LineStyleOpts(),
|
|
150
|
+
)
|
|
151
|
+
)
|
|
137
152
|
|
|
138
153
|
graph = (
|
|
139
154
|
Graph()
|
|
@@ -143,6 +158,7 @@ def nx_digraph_to_graph(
|
|
|
143
158
|
links=links,
|
|
144
159
|
layout=layout,
|
|
145
160
|
is_draggable=True,
|
|
161
|
+
edge_symbol=graph_opts.pop("edge_symbol", ["circle", "arrow"]),
|
|
146
162
|
**graph_opts,
|
|
147
163
|
)
|
|
148
164
|
.set_global_opts(
|
|
@@ -164,6 +180,8 @@ def nx_digraph_to_tree(
|
|
|
164
180
|
color_key: str = "color",
|
|
165
181
|
default_name: str = "node",
|
|
166
182
|
virtual_root_id: Optional[str] = None,
|
|
183
|
+
initial_tree_depth: int | None = None,
|
|
184
|
+
tooltip_opts=opts.TooltipOpts(trigger="item", formatter="{b}"),
|
|
167
185
|
) -> Tree:
|
|
168
186
|
"""
|
|
169
187
|
将树状结构的 networkx.DiGraph 转换为 Pyecharts Tree 图表
|
|
@@ -196,6 +214,7 @@ def nx_digraph_to_tree(
|
|
|
196
214
|
def build_node(current: Any) -> Dict[str, Any]:
|
|
197
215
|
attrs = G.nodes.get(current, {})
|
|
198
216
|
node = {
|
|
217
|
+
"nodeAttrs": attrs,
|
|
199
218
|
"name": attrs.get(name_key, str(current) or default_name),
|
|
200
219
|
"itemStyle": {"color": attrs[color_key]} if color_key in attrs else {},
|
|
201
220
|
}
|
|
@@ -207,7 +226,7 @@ def nx_digraph_to_tree(
|
|
|
207
226
|
tree = Tree().set_global_opts(
|
|
208
227
|
# datazoom_opts=opts.DataZoomOpts(orient="vertical"),# not work
|
|
209
228
|
title_opts=opts.TitleOpts(title=title),
|
|
210
|
-
tooltip_opts=
|
|
229
|
+
tooltip_opts=tooltip_opts,
|
|
211
230
|
)
|
|
212
231
|
for root in roots:
|
|
213
232
|
tree_data = [build_node(root)]
|
|
@@ -221,6 +240,7 @@ def nx_digraph_to_tree(
|
|
|
221
240
|
vertical_align="middle",
|
|
222
241
|
horizontal_align="right",
|
|
223
242
|
),
|
|
243
|
+
initial_tree_depth=initial_tree_depth,
|
|
224
244
|
)
|
|
225
245
|
|
|
226
246
|
return tree
|
ipa/pkg/__init__.py
CHANGED
|
@@ -1,43 +1,99 @@
|
|
|
1
1
|
import inspect
|
|
2
|
+
import logging
|
|
3
|
+
from importlib import import_module
|
|
2
4
|
from types import ModuleType
|
|
3
|
-
from typing import List, Tuple, Type, TypeVar
|
|
5
|
+
from typing import Callable, List, Optional, Set, Tuple, Type, TypeVar
|
|
4
6
|
|
|
5
|
-
T = TypeVar("T")
|
|
6
7
|
|
|
8
|
+
def try_import(path: str):
|
|
9
|
+
"""
|
|
10
|
+
支持以x.y.z的方式导入模块或模块内的属性
|
|
11
|
+
Args:
|
|
12
|
+
path: 模块路径或属性路径
|
|
13
|
+
Returns:
|
|
14
|
+
导入的模块或属性, 如果导入失败则返回None
|
|
15
|
+
"""
|
|
16
|
+
try:
|
|
17
|
+
return import_module(path)
|
|
18
|
+
except ModuleNotFoundError:
|
|
19
|
+
tmps = path.split(".")
|
|
20
|
+
assert len(tmps) > 1, f"unexpected path: {path}"
|
|
21
|
+
module = try_import(".".join(tmps[0:-1]))
|
|
22
|
+
return getattr(module, tmps[-1], None)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
logging.warning("failed to import %s: %s", path, e)
|
|
25
|
+
return None
|
|
7
26
|
|
|
8
|
-
|
|
27
|
+
|
|
28
|
+
def try_import(path: str):
|
|
9
29
|
"""
|
|
10
|
-
|
|
30
|
+
支持以x.y.z的方式导入模块或模块内的属性
|
|
31
|
+
Args:
|
|
32
|
+
path: 模块路径或属性路径
|
|
33
|
+
Returns:
|
|
34
|
+
导入的模块或属性, 如果导入失败则返回None
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
return import_module(path)
|
|
38
|
+
except ModuleNotFoundError:
|
|
39
|
+
tmps = path.split(".")
|
|
40
|
+
assert len(tmps) > 1, f"unexpected path: {path}"
|
|
41
|
+
module = try_import(".".join(tmps[0:-1]))
|
|
42
|
+
return getattr(module, tmps[-1], None)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
logging.warning("failed to import %s: %s", path, e)
|
|
45
|
+
return None
|
|
11
46
|
|
|
12
|
-
参数:
|
|
13
|
-
cls: 目标类(需传入类本身,而非实例)
|
|
14
47
|
|
|
15
|
-
|
|
16
|
-
|
|
48
|
+
T = TypeVar("T")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def is_subclass_of(
|
|
52
|
+
child: Type[T], parent: Type[T], include_parent: bool = True
|
|
53
|
+
) -> bool:
|
|
17
54
|
"""
|
|
18
|
-
|
|
19
|
-
|
|
55
|
+
判断child是否是parent的子类
|
|
56
|
+
Args:
|
|
57
|
+
child: 子类
|
|
58
|
+
parent: 父类
|
|
59
|
+
include_parent: 父类本身是否视为子类
|
|
60
|
+
Returns:
|
|
61
|
+
是否是子类
|
|
62
|
+
"""
|
|
63
|
+
if (not include_parent) and (child is parent):
|
|
64
|
+
return False
|
|
65
|
+
return inspect.isclass(child) and issubclass(child, parent)
|
|
20
66
|
|
|
21
67
|
|
|
22
68
|
def collect_subclass_of(
|
|
23
|
-
|
|
69
|
+
parent: Type[T], _from: ModuleType, include_parent: bool = False
|
|
24
70
|
):
|
|
25
71
|
"""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
返回:
|
|
34
|
-
List[Tuple[str, T]]: 包含子类名和子类对象的元组列表
|
|
72
|
+
收集所有子类
|
|
73
|
+
Args:
|
|
74
|
+
parent: 父类
|
|
75
|
+
_from: 模块
|
|
76
|
+
include_parent: 父类本身是否视为子类
|
|
77
|
+
Returns:
|
|
78
|
+
所有子类的(名称,类,全路径)元组
|
|
35
79
|
"""
|
|
36
80
|
|
|
37
|
-
def predicate(
|
|
38
|
-
|
|
39
|
-
return False
|
|
40
|
-
return inspect.isclass(m) and issubclass(m, base_class)
|
|
81
|
+
def predicate(child):
|
|
82
|
+
return is_subclass_of(child=child, parent=parent, include_parent=include_parent)
|
|
41
83
|
|
|
42
84
|
name_class_map: List[Tuple[str, T]] = inspect.getmembers(_from, predicate=predicate)
|
|
43
85
|
return name_class_map
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_class_full_path(cls) -> str:
|
|
89
|
+
"""
|
|
90
|
+
获取类的全路径表示(模块路径.类名)
|
|
91
|
+
|
|
92
|
+
参数:
|
|
93
|
+
cls: 目标类(需传入类本身,而非实例)
|
|
94
|
+
|
|
95
|
+
返回:
|
|
96
|
+
str: 类的全路径,格式为 "模块路径.类名"
|
|
97
|
+
"""
|
|
98
|
+
# __qualname__ 获取类的限定名(含嵌套关系,如 "OuterClass.InnerClass")
|
|
99
|
+
return f"{cls.__module__}.{cls.__qualname__}"
|
ipa/pkg/hot_reload.py
CHANGED
|
@@ -2,7 +2,6 @@ import logging
|
|
|
2
2
|
from importlib import import_module
|
|
3
3
|
from typing import Callable, Optional, Set, Tuple
|
|
4
4
|
|
|
5
|
-
import hmr
|
|
6
5
|
from watchfiles import Change, PythonFilter, awatch
|
|
7
6
|
|
|
8
7
|
|
|
@@ -13,10 +12,13 @@ def try_hot_reload(name, package: Optional[str] = None):
|
|
|
13
12
|
name: 模块名
|
|
14
13
|
package: 包名
|
|
15
14
|
"""
|
|
16
|
-
|
|
17
15
|
try:
|
|
18
16
|
hmlib = import_module(name=name, package=package)
|
|
17
|
+
assert hmlib, f"failed to import module: {package}.{name}"
|
|
19
18
|
logging.info("shall reload: %s", hmlib)
|
|
19
|
+
# TODO: exclude怎么用
|
|
20
|
+
import hmr
|
|
21
|
+
|
|
20
22
|
hmr.reload(hmlib)
|
|
21
23
|
except Exception as e:
|
|
22
24
|
logging.warning("failed to reload: %s", e)
|
ipa/pydantic/core/__init__.py
CHANGED
ipa/version.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
3
4
|
|
|
4
5
|
__all__ = [
|
|
5
6
|
"__version__",
|
|
@@ -10,25 +11,14 @@ __all__ = [
|
|
|
10
11
|
"commit_id",
|
|
11
12
|
]
|
|
12
13
|
|
|
13
|
-
TYPE_CHECKING = False
|
|
14
|
-
if TYPE_CHECKING:
|
|
15
|
-
from typing import Tuple
|
|
16
|
-
from typing import Union
|
|
17
|
-
|
|
18
|
-
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
-
COMMIT_ID = Union[str, None]
|
|
20
|
-
else:
|
|
21
|
-
VERSION_TUPLE = object
|
|
22
|
-
COMMIT_ID = object
|
|
23
|
-
|
|
24
14
|
version: str
|
|
25
15
|
__version__: str
|
|
26
|
-
__version_tuple__:
|
|
27
|
-
version_tuple:
|
|
28
|
-
commit_id:
|
|
29
|
-
__commit_id__:
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
30
20
|
|
|
31
|
-
__version__ = version = '0.1
|
|
32
|
-
__version_tuple__ = version_tuple = (
|
|
21
|
+
__version__ = version = '1.0.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 1)
|
|
33
23
|
|
|
34
24
|
__commit_id__ = commit_id = None
|
ipa/websocket/core/__init__.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from typing import Union
|
|
2
|
+
from typing import TypeAlias, Union
|
|
3
3
|
|
|
4
|
-
import fastapi
|
|
5
4
|
import websockets
|
|
6
5
|
from websockets.asyncio.client import connect
|
|
7
6
|
|
|
@@ -11,15 +10,19 @@ try:
|
|
|
11
10
|
except:
|
|
12
11
|
from websockets import ClientConnection, ServerConnection
|
|
13
12
|
|
|
13
|
+
try:
|
|
14
|
+
from fastapi import WebSocket
|
|
15
|
+
except:
|
|
16
|
+
WebSocket: TypeAlias = None
|
|
14
17
|
|
|
15
|
-
WebSocketConnection = Union[ServerConnection,
|
|
18
|
+
WebSocketConnection = Union[ServerConnection, WebSocket, ClientConnection]
|
|
16
19
|
|
|
17
20
|
|
|
18
21
|
def is_websocket_closed(conn: WebSocketConnection):
|
|
19
22
|
"""
|
|
20
23
|
判断WebSocket连接是否已关闭
|
|
21
24
|
"""
|
|
22
|
-
if isinstance(conn,
|
|
25
|
+
if isinstance(conn, WebSocket):
|
|
23
26
|
from starlette.websockets import WebSocketState
|
|
24
27
|
|
|
25
28
|
return conn.client_state == WebSocketState.DISCONNECTED
|
|
@@ -34,7 +37,7 @@ def is_websocket_closed(conn: WebSocketConnection):
|
|
|
34
37
|
async def send_text(conn: WebSocketConnection, data: str):
|
|
35
38
|
if isinstance(conn, ServerConnection):
|
|
36
39
|
await conn.send(data)
|
|
37
|
-
elif isinstance(conn,
|
|
40
|
+
elif isinstance(conn, WebSocket):
|
|
38
41
|
await conn.send_text(data)
|
|
39
42
|
else:
|
|
40
43
|
raise NotImplementedError(
|
|
@@ -45,7 +48,7 @@ async def send_text(conn: WebSocketConnection, data: str):
|
|
|
45
48
|
def get_close_code(conn: WebSocketConnection):
|
|
46
49
|
if isinstance(conn, ServerConnection):
|
|
47
50
|
return conn.close_code
|
|
48
|
-
elif isinstance(conn,
|
|
51
|
+
elif isinstance(conn, WebSocket):
|
|
49
52
|
return None
|
|
50
53
|
|
|
51
54
|
|
|
@@ -55,7 +58,7 @@ def parse_client_info(connection: WebSocketConnection):
|
|
|
55
58
|
"""
|
|
56
59
|
if isinstance(connection, ServerConnection):
|
|
57
60
|
client_ip, client_port = connection.transport.get_extra_info("peername")
|
|
58
|
-
elif isinstance(connection,
|
|
61
|
+
elif isinstance(connection, WebSocket):
|
|
59
62
|
client_ip, client_port = connection.client.host, connection.client.port
|
|
60
63
|
else:
|
|
61
64
|
client_ip, client_port = None, None
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ipa-python-kit
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 1.0.1
|
|
4
4
|
Summary: kit for python
|
|
5
5
|
Author-email: xdewx <present150608@sina.com>
|
|
6
6
|
Keywords: python,kit,lib,util
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
Requires-Dist: deprecated>=1.3.1
|
|
10
10
|
Requires-Dist: pint>=0.21.1
|
|
@@ -43,11 +43,19 @@ Requires-Dist: ipa-python-kit[visualization]; extra == "all"
|
|
|
43
43
|
Provides-Extra: des
|
|
44
44
|
Requires-Dist: simpy>=4.1.1; extra == "des"
|
|
45
45
|
|
|
46
|
-
#
|
|
46
|
+
# ipa-python-kit
|
|
47
|
+
|
|
48
|
+
## usage
|
|
49
|
+
|
|
50
|
+
`import ipa`
|
|
51
|
+
|
|
52
|
+
or
|
|
53
|
+
|
|
54
|
+
`from ipa.xx import yy`
|
|
47
55
|
|
|
48
56
|
## features
|
|
49
57
|
|
|
50
|
-
1. pytest
|
|
58
|
+
1. pytest & pytest-cov
|
|
51
59
|
2. pre-commit
|
|
52
60
|
3. darker for code format
|
|
53
61
|
4. ruff for lint
|
|
@@ -58,20 +66,24 @@ Requires-Dist: simpy>=4.1.1; extra == "des"
|
|
|
58
66
|
|
|
59
67
|
## introduction
|
|
60
68
|
|
|
61
|
-
the project uses `src-layout`, includes `
|
|
62
|
-
|
|
69
|
+
the project uses `src-layout`, includes `my_sdk` (for publish) and `biz` (for business logic).
|
|
70
|
+
|
|
71
|
+
~~`sdk` is designed for publish and no need to rename, we use file/folder mapping `my_sdk=src/sdk` in `pyproject.toml`.~~
|
|
72
|
+
|
|
73
|
+
feel free to rename `my_sdk` to `[the name you want]` in the whole project for your own use or publish.
|
|
63
74
|
|
|
64
75
|
### dev
|
|
65
76
|
|
|
66
77
|
before you start:
|
|
67
78
|
|
|
68
79
|
1. must run `./scripts/setup`
|
|
69
|
-
1.
|
|
70
|
-
1. remember to `uv pip install -e .[all]` to make sure `
|
|
80
|
+
1. replace `my_sdk` with `[the name you want]` in the whole project
|
|
81
|
+
1. ~~remember to `uv pip install -e .[all]` to make sure `my_sdk` is available in development.~~
|
|
71
82
|
|
|
72
83
|
### unit test
|
|
73
84
|
|
|
74
85
|
`uv run -m pytest`
|
|
86
|
+
> `htmlcov` folder contains html coverage report.
|
|
75
87
|
|
|
76
88
|
### run cli
|
|
77
89
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
ipa/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
ipa/version.py,sha256=
|
|
2
|
+
ipa/version.py,sha256=9c7Tl9g3gfjrXmE7H04a_dPracz6dNMOSBdT2fhCzVw,544
|
|
3
|
+
ipa/app/__init__.py,sha256=PsYzgVAzJSYDUopPAnMe7pMVMcpyOC90NYrqZ-XIyNY,1539
|
|
3
4
|
ipa/cli/__init__.py,sha256=YYZYz2QwK34Ro-EBrEDd4sZJ6IeqvUCP-b2cjPWbPa8,44
|
|
4
5
|
ipa/cli/model.py,sha256=Tdvn8XWcJ_aoghf5N8PlLc38-O01_XPlef66i_--z08,2584
|
|
5
6
|
ipa/cli/proxy.py,sha256=rEqxH9RacIrb2lxglCnihmF2RooT719WE3NGur5hIfE,7131
|
|
@@ -21,24 +22,21 @@ ipa/data_type/page.py,sha256=8gixlNo2YgY41BK2lwBm84BJUEp9BW4OoT4cr6AJ-mw,1389
|
|
|
21
22
|
ipa/data_type/point.py,sha256=wJrXONLstZubS3bqRBJCskj268PV5k92qzkk-5YOWgc,9748
|
|
22
23
|
ipa/data_type/process.py,sha256=Wgr9gmY_Q6UBIYEtGY5259iqhBZ9oJZVoSUwMIancdY,1826
|
|
23
24
|
ipa/data_type/spatial.py,sha256=TcehRvqxpWzo4iBZjnXSBoGTejJaR_5bgw-2goidXlI,1408
|
|
24
|
-
ipa/data_type/unitable.py,sha256=
|
|
25
|
-
ipa/datetime/__init__.py,sha256=
|
|
26
|
-
ipa/datetime/core.py,sha256=
|
|
25
|
+
ipa/data_type/unitable.py,sha256=pZ8-rEx2h8SLvO7xAaAT52JX2608pwkempPLlBT2h7s,12613
|
|
26
|
+
ipa/datetime/__init__.py,sha256=FDmUO-LWFrTRKZlVv2mVCP_KUClURpSWUXE9Vn9fs34,672
|
|
27
|
+
ipa/datetime/core.py,sha256=jwkljzWPX74WOa004Ply9HVffXEAr7aVMg0sBvoef5k,1739
|
|
27
28
|
ipa/decorator/__init__.py,sha256=vD9dHyCtblfLmOPCJ-cAAybWwdMtG4IVdZmbald52yI,177
|
|
28
29
|
ipa/decorator/exception.py,sha256=vvNt-LsgU8hM3NM6eJZfFdk8ZTxgJiXDAz2tfnUdlug,1525
|
|
29
30
|
ipa/echarts/__init__.py,sha256=nE8jJjGJaKuJKCyuf6QLfrj8xPj8IqGKeH2VXP3FMzY,21
|
|
30
31
|
ipa/echarts/core/__init__.py,sha256=7k4ayFIkBg5SeKC35d1Jm_XNT03G1QaRF42Qf7jHm-8,6161
|
|
31
|
-
ipa/extra/simpy/__init__.py,sha256=nE8jJjGJaKuJKCyuf6QLfrj8xPj8IqGKeH2VXP3FMzY,21
|
|
32
|
-
ipa/extra/simpy/core.py,sha256=9UE2W4_j_gmtm0lD7DVJmTNu-txJSiI9z6Rj7nGjeb4,8724
|
|
33
32
|
ipa/fastapi/__init__.py,sha256=nE8jJjGJaKuJKCyuf6QLfrj8xPj8IqGKeH2VXP3FMzY,21
|
|
34
33
|
ipa/fastapi/core/__init__.py,sha256=FjrwqRWQ353UToZM6nEfc2awpJkMxDHFTb4SIPyTjtU,303
|
|
35
34
|
ipa/func/__init__.py,sha256=2Nhw9QKqMXozWhEJltO-8_vl8aVCkdi2nnkXaZVAQMk,162
|
|
36
35
|
ipa/func/_async.py,sha256=hiaTvncAkuFZl_UG_sLlaLkU4j6M3HD8i9rCxa4WtAo,594
|
|
37
36
|
ipa/func/_lambda.py,sha256=S2aa0FE4n8ZSEMHe4bc6bUq1bZU6vb0eVaBrQtLwLr0,1321
|
|
38
37
|
ipa/func/_object.py,sha256=dD8LzxHnA3oVarMeHm-Mloc0RrivE1qKPrtXhtUO0rM,1025
|
|
39
|
-
ipa/func/app.py,sha256=_55uIeg_7z7VA3dPPEMCoh683tWi1bP5-m-_IPqKMmg,304
|
|
40
38
|
ipa/func/database.py,sha256=RAADcLKF4te0mOHh-5qCTkL14As7njeYUn8PuJB3_XQ,160
|
|
41
|
-
ipa/func/dict.py,sha256=
|
|
39
|
+
ipa/func/dict.py,sha256=jleT0jC2FSoykauQHavre_Qic4yEGbJ5xf7qQJV_1c8,3022
|
|
42
40
|
ipa/func/num.py,sha256=PmOAJIeaXS5Maw4rLpC9RCoTIFansnMjPKEsukbGWIc,1620
|
|
43
41
|
ipa/func/shortcut.py,sha256=K3QjNbbd4puS5c8DBzhj8Qbwh_gqufFwfhOwm4HrXnQ,639
|
|
44
42
|
ipa/func/table.py,sha256=wAeiJazUGuMHhevDKY6pFzR47Ric95IWE_mljo07TQU,323
|
|
@@ -47,28 +45,28 @@ ipa/i18n/core.py,sha256=qkp4HYKcmYxar5Ztbyy11vIX2QpmMhAA-ZD0XtXYjgY,710
|
|
|
47
45
|
ipa/i18n/locales/__init__.py,sha256=um3B6elBvsWhWNxlZlrbzVtfdA4Bd9MuPyPOe3KxfU8,104
|
|
48
46
|
ipa/io/__init__.py,sha256=vqHQtntss1eXsM1sJaC8LNWK3Jh9S2SmSTVBRiIM7HI,23
|
|
49
47
|
ipa/io/buffer.py,sha256=2a18Qmx5SfNQB_nDK8BEQc5xMn7L5x0qQuDAVg-_6Xk,7347
|
|
50
|
-
ipa/
|
|
51
|
-
ipa/
|
|
52
|
-
ipa/
|
|
53
|
-
ipa/
|
|
54
|
-
ipa/
|
|
55
|
-
ipa/
|
|
56
|
-
ipa/
|
|
48
|
+
ipa/logging/__init__.py,sha256=ejMSLTUiIDtCYh69INFx84Byfmse6X2VDtiuWGjICy8,1579
|
|
49
|
+
ipa/logging/builder.py,sha256=AnfpSchz3hzMHk8kdsUwCDLa4WzrXFtBxMXjSBsYZaU,5793
|
|
50
|
+
ipa/logging/fmt.py,sha256=mvB02qhFY20QOMkownG9MUHsUBKS1j6SWpqS7sHzQVE,649
|
|
51
|
+
ipa/logging/handler.py,sha256=d9bifBms4Bn_Iwl71RkBDix6Rm8aQO4pCcBCpATJ3Mo,488
|
|
52
|
+
ipa/logging/util.py,sha256=fMnxe_7hnQmY1AtFZs_zwqihMOdS60jbaoCNSi_GUzo,9868
|
|
53
|
+
ipa/logging/model/__init__.py,sha256=xngWypB4IA8Y9jR9XfVftRxIeG5DsXQ34xZ5sE8pHxQ,28
|
|
54
|
+
ipa/logging/model/dict_config.py,sha256=MlFEv761AOptxv_-s6GOWWcNECmVHpjLb9OfExRDJ4c,1815
|
|
57
55
|
ipa/math/__init__.py,sha256=88LjYXcDsH6pV1RwwQVKFGkY3NH3PGjVynWlD9JigBQ,239
|
|
58
56
|
ipa/math/core.py,sha256=8wp_Sa8XybdAR914OxVm8FiTv4A0QFa0khkz0p4qpqw,720
|
|
59
|
-
ipa/network/__init__.py,sha256=
|
|
60
|
-
ipa/pkg/__init__.py,sha256=
|
|
61
|
-
ipa/pkg/hot_reload.py,sha256=
|
|
57
|
+
ipa/network/__init__.py,sha256=yxuS7OoEeBgXojJzvdJOufzWaKOnm-RWUKXqGQV8nNo,11208
|
|
58
|
+
ipa/pkg/__init__.py,sha256=V2-AAF0JITA3Qb-eK2EgX4iExURjD570ZUV8s03WtT0,2881
|
|
59
|
+
ipa/pkg/hot_reload.py,sha256=OCMdx6kxpGUfi2edi6S_KM_uZI-td6FYYaqGh-4RIKc,1045
|
|
62
60
|
ipa/pydantic/__init__.py,sha256=nE8jJjGJaKuJKCyuf6QLfrj8xPj8IqGKeH2VXP3FMzY,21
|
|
63
|
-
ipa/pydantic/core/__init__.py,sha256=
|
|
61
|
+
ipa/pydantic/core/__init__.py,sha256=0j74mrC93tbczjJeyonIlRQai1EbyFCabM-i_wrKg0A,2111
|
|
64
62
|
ipa/system/__init__.py,sha256=iTpqRchOpz1GLEY5AvQt0wEGL0dFCz1bczRcBwj7U7k,45
|
|
65
63
|
ipa/system/disk.py,sha256=CkR7WWEJOcTeeVJXQdHOxd9kXJgvOYLIRam1TdPAWv4,955
|
|
66
64
|
ipa/system/process.py,sha256=E2RRt1cMhjC2_ZDseU-sU52VXq2DVxpn0umturaQoeA,5885
|
|
67
65
|
ipa/visualization/__init__.py,sha256=FnTG9eLlbkd4vPyyzteOMjrm3IEtdOYYRUAg0hw-ejg,26
|
|
68
66
|
ipa/visualization/_networkx.py,sha256=5bUbKTtTwdFYgEY1ww99-fvVjvWhJkfTf1sVYfUax8k,1598
|
|
69
67
|
ipa/websocket/__init__.py,sha256=nE8jJjGJaKuJKCyuf6QLfrj8xPj8IqGKeH2VXP3FMzY,21
|
|
70
|
-
ipa/websocket/core/__init__.py,sha256=
|
|
71
|
-
ipa_python_kit-0.1.
|
|
72
|
-
ipa_python_kit-0.1.
|
|
73
|
-
ipa_python_kit-0.1.
|
|
74
|
-
ipa_python_kit-0.1.
|
|
68
|
+
ipa/websocket/core/__init__.py,sha256=EOqo4MnWXdjHcmqiP61pWIinbhDYb4_Lesiei7uiyXk,4843
|
|
69
|
+
ipa_python_kit-1.0.1.dist-info/METADATA,sha256=M3YWGy74OQ5jqCd0zu34bZS5SmVGF6YJ0py3sBmilMM,2910
|
|
70
|
+
ipa_python_kit-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
71
|
+
ipa_python_kit-1.0.1.dist-info/top_level.txt,sha256=jpjQhUSwP61wKQAysY61KwKKom42vtQTiJNXMT6AyCc,4
|
|
72
|
+
ipa_python_kit-1.0.1.dist-info/RECORD,,
|
ipa/extra/simpy/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from .core import *
|
ipa/extra/simpy/core.py
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import math
|
|
3
|
-
import time
|
|
4
|
-
from abc import ABC
|
|
5
|
-
from typing import Any, List, Optional, Union
|
|
6
|
-
|
|
7
|
-
import simpy
|
|
8
|
-
import simpy.resources
|
|
9
|
-
import simpy.resources.resource
|
|
10
|
-
from simpy.core import EmptySchedule, StopSimulation
|
|
11
|
-
from simpy.events import URGENT
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def try_put(
|
|
15
|
-
env: simpy.Environment,
|
|
16
|
-
container: simpy.Container,
|
|
17
|
-
amount: Union[int, float],
|
|
18
|
-
timeout: Optional[int] = None,
|
|
19
|
-
):
|
|
20
|
-
x = container.put(amount)
|
|
21
|
-
if timeout:
|
|
22
|
-
x = x | env.timeout(timeout)
|
|
23
|
-
v = yield x
|
|
24
|
-
return v
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def get_free_capacity(container: simpy.Container):
|
|
28
|
-
"""
|
|
29
|
-
获取容器的空闲容量
|
|
30
|
-
Args:
|
|
31
|
-
container (simpy.Container): 容器
|
|
32
|
-
Returns:
|
|
33
|
-
int | float: 空闲容量
|
|
34
|
-
"""
|
|
35
|
-
# TODO:这里预先触发存取操作,可能会有性能影响,后续可考虑分离出去
|
|
36
|
-
container._trigger_put(None)
|
|
37
|
-
container._trigger_get(None)
|
|
38
|
-
return container.capacity - container.level
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
def resize_container(
|
|
42
|
-
container: simpy.Container, capacity: Union[int, float], is_delta=False
|
|
43
|
-
):
|
|
44
|
-
"""
|
|
45
|
-
调整容器容量
|
|
46
|
-
Args:
|
|
47
|
-
container (simpy.Container): 容器
|
|
48
|
-
capacity (int | float): 新容量
|
|
49
|
-
is_delta (bool, optional): 是否是增量调整. 默认False
|
|
50
|
-
"""
|
|
51
|
-
old_capacity = container.capacity
|
|
52
|
-
capacity = capacity + old_capacity if is_delta else capacity
|
|
53
|
-
dx = capacity - old_capacity
|
|
54
|
-
if not dx:
|
|
55
|
-
return
|
|
56
|
-
# 如果是减容,先触发取操作
|
|
57
|
-
if dx < 0:
|
|
58
|
-
container._trigger_get(None)
|
|
59
|
-
assert (
|
|
60
|
-
container.level <= capacity
|
|
61
|
-
), f"容器当前等级{container.level}大于新容量{capacity},无法完成减容"
|
|
62
|
-
container._capacity = capacity
|
|
63
|
-
if dx > 0:
|
|
64
|
-
container._trigger_put(None)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def resource_to_dict(resource: Any):
|
|
68
|
-
if isinstance(resource, simpy.Container):
|
|
69
|
-
return dict(level=resource.level, capacity=resource.capacity)
|
|
70
|
-
if isinstance(resource, simpy.Resource):
|
|
71
|
-
return dict(level=resource.count, capacity=resource.capacity)
|
|
72
|
-
if isinstance(resource, simpy.Store):
|
|
73
|
-
return dict(level=len(resource.items), capacity=resource.capacity)
|
|
74
|
-
print("unexpected type:" + resource)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def schedule_event_at(
|
|
78
|
-
env: simpy.Environment, at: Union[float, int], ev: Optional[simpy.Event] = None
|
|
79
|
-
):
|
|
80
|
-
"""
|
|
81
|
-
计划在指定时间触发事件
|
|
82
|
-
Args:
|
|
83
|
-
env (simpy.Environment): 仿真环境
|
|
84
|
-
at (float | int): 触发时间
|
|
85
|
-
ev (simpy.Event | None, optional): 事件. 默认None
|
|
86
|
-
"""
|
|
87
|
-
if at < env.now:
|
|
88
|
-
return
|
|
89
|
-
if ev is None:
|
|
90
|
-
ev = env.event()
|
|
91
|
-
ev._ok = True
|
|
92
|
-
ev._value = None
|
|
93
|
-
env.schedule(ev, URGENT, at - env.now)
|
|
94
|
-
return ev
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def interrupt_on_event(env: simpy.Environment, ev: simpy.Event):
|
|
98
|
-
"""
|
|
99
|
-
当事件触发时中断仿真
|
|
100
|
-
Args:
|
|
101
|
-
env (simpy.Environment): 仿真环境
|
|
102
|
-
ev (simpy.Event): 事件
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
raise NotImplementedError("interrupt_on_event not implemented")
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def interrupt_if_timeout(
|
|
109
|
-
env: simpy.Environment,
|
|
110
|
-
timeout: Optional[float] = None,
|
|
111
|
-
check_interval: float = 10,
|
|
112
|
-
exit_signal: Optional[simpy.Event] = None,
|
|
113
|
-
):
|
|
114
|
-
"""
|
|
115
|
-
根据仿真真实运行时间是否超时来中断仿真
|
|
116
|
-
|
|
117
|
-
Args:
|
|
118
|
-
env (simpy.Environment): 仿真环境
|
|
119
|
-
timeout (float | None, optional): 最大运行时间. 默认不限时
|
|
120
|
-
check_interval (float, optional): 这个时间是仿真中的时间
|
|
121
|
-
exit_signal (simpy.Event | None, optional): 外部信号,用于外部中断仿真. 默认不使用.
|
|
122
|
-
"""
|
|
123
|
-
|
|
124
|
-
def inner():
|
|
125
|
-
cpu_start_time = time.time()
|
|
126
|
-
while True:
|
|
127
|
-
dt = time.time() - cpu_start_time
|
|
128
|
-
if timeout is not None and dt > timeout:
|
|
129
|
-
raise simpy.Interrupt(
|
|
130
|
-
f"仿真运行时间{dt}超过最大运行时间{timeout},强制退出"
|
|
131
|
-
)
|
|
132
|
-
if exit_signal and exit_signal.triggered:
|
|
133
|
-
raise simpy.Interrupt(f"外部信号{exit_signal}触发,强制退出")
|
|
134
|
-
yield env.timeout(check_interval)
|
|
135
|
-
|
|
136
|
-
return env.process(inner())
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def has_no_event_left(env: simpy.Environment):
|
|
140
|
-
"""
|
|
141
|
-
判断指定环境中是否还有未处理的事件
|
|
142
|
-
"""
|
|
143
|
-
return not env._queue and not env.active_process
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
class DeadlineException(StopSimulation):
|
|
147
|
-
"""
|
|
148
|
-
截止事件异常
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
def __init__(self, cause):
|
|
152
|
-
super().__init__(cause)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
class DeadlineEvent(simpy.Event):
|
|
156
|
-
"""
|
|
157
|
-
截止事件,用于在指定时间触发**DeadlineException**异常从而正常退出
|
|
158
|
-
如果仿真早于截止时间完成, 则此事件仍会执行
|
|
159
|
-
"""
|
|
160
|
-
|
|
161
|
-
def interrupt(self, *args, **kwargs):
|
|
162
|
-
"""
|
|
163
|
-
中断事件
|
|
164
|
-
"""
|
|
165
|
-
# 如果后续还有事件,忽略事件正常退出,此时时钟停止在deadline时间
|
|
166
|
-
raise DeadlineException(f"deadline {self.at} reached, force exit")
|
|
167
|
-
|
|
168
|
-
def __init__(
|
|
169
|
-
self,
|
|
170
|
-
env: simpy.Environment,
|
|
171
|
-
at: Union[float, int, None] = None,
|
|
172
|
-
):
|
|
173
|
-
super().__init__(env)
|
|
174
|
-
self.at: Union[float, int, None] = None
|
|
175
|
-
self._ok = True
|
|
176
|
-
self._value = None
|
|
177
|
-
if at is not None:
|
|
178
|
-
self.schedule_at(at)
|
|
179
|
-
# self.callbacks = [StopSimulation.callback]
|
|
180
|
-
self.callbacks = [self.interrupt]
|
|
181
|
-
|
|
182
|
-
def schedule_at(self, at: Union[float, int]):
|
|
183
|
-
"""
|
|
184
|
-
计划在指定时间触发事件
|
|
185
|
-
Args:
|
|
186
|
-
at (float | int): 触发时间
|
|
187
|
-
"""
|
|
188
|
-
if self.at is not None:
|
|
189
|
-
raise ValueError("deadline event already scheduled")
|
|
190
|
-
self.at = at
|
|
191
|
-
return schedule_event_at(self.env, at, self)
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def run_until(
|
|
195
|
-
env: simpy.Environment,
|
|
196
|
-
exit_signal: Optional[simpy.Event] = None,
|
|
197
|
-
max_sim_time: Optional[float] = None,
|
|
198
|
-
extra_events: Optional[List[simpy.Event]] = None,
|
|
199
|
-
):
|
|
200
|
-
"""
|
|
201
|
-
|
|
202
|
-
运行仿真直到指定事件触发或仿真时间超过最大运行时间
|
|
203
|
-
|
|
204
|
-
TODO: 如果重复调用,可能添加多次until事件,如何规避?
|
|
205
|
-
|
|
206
|
-
Args:
|
|
207
|
-
env (simpy.Environment): 仿真环境
|
|
208
|
-
exit_signal (simpy.Event | None, optional): 外部信号,用于外部中断仿真. 默认不使用.
|
|
209
|
-
max_sim_time (float | None, optional): 最大运行时间. 默认不限时
|
|
210
|
-
extra_events (List[simpy.Event] | None, optional): 额外事件. 默认不使用.
|
|
211
|
-
"""
|
|
212
|
-
if getattr(env, "__old_step__", None) is None:
|
|
213
|
-
env.__old_step__ = env.step
|
|
214
|
-
|
|
215
|
-
def _step():
|
|
216
|
-
next_time = env.peek()
|
|
217
|
-
if next_time is None or next_time == math.inf:
|
|
218
|
-
raise EmptySchedule from None
|
|
219
|
-
if next_time > env.__max_sim_time__:
|
|
220
|
-
raise DeadlineException(
|
|
221
|
-
f"next simulation time {next_time} will exceeds max_sim_time {max_sim_time}"
|
|
222
|
-
)
|
|
223
|
-
env.__old_step__()
|
|
224
|
-
|
|
225
|
-
events = []
|
|
226
|
-
if exit_signal:
|
|
227
|
-
events.append(exit_signal)
|
|
228
|
-
if max_sim_time is not None:
|
|
229
|
-
env.__max_sim_time__ = max_sim_time
|
|
230
|
-
env.step = _step
|
|
231
|
-
# 这种方式会将事件加入调度队列,还是会造成仿真延迟
|
|
232
|
-
# # dt = max(0, max_sim_time - env.now)
|
|
233
|
-
# # events.append(env.timeout(dt))
|
|
234
|
-
# events.append(DeadlineEvent(env, max_sim_time))
|
|
235
|
-
if extra_events:
|
|
236
|
-
events.extend(extra_events)
|
|
237
|
-
until = env.any_of(events) if events else None
|
|
238
|
-
try:
|
|
239
|
-
env.run(until=until)
|
|
240
|
-
except Exception as e:
|
|
241
|
-
if has_no_event_left(env):
|
|
242
|
-
logging.warning(
|
|
243
|
-
"error happens but there is no event left, will ignore: %s", e
|
|
244
|
-
)
|
|
245
|
-
return
|
|
246
|
-
raise e
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def stop_simulation(env: simpy.Environment):
|
|
250
|
-
"""
|
|
251
|
-
停止仿真
|
|
252
|
-
1. 清空事件队列,诱发底层异常
|
|
253
|
-
"""
|
|
254
|
-
|
|
255
|
-
def clear_queue():
|
|
256
|
-
size = len(env._queue)
|
|
257
|
-
logging.debug("will remove %s events in queue", size)
|
|
258
|
-
env._queue.clear()
|
|
259
|
-
yield env.timeout(0)
|
|
260
|
-
return size
|
|
261
|
-
|
|
262
|
-
return env.process(clear_queue())
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
class ISimLock(ABC):
|
|
266
|
-
|
|
267
|
-
def __init__(self, env: simpy.Environment):
|
|
268
|
-
self._occupied_ = simpy.Resource(env, capacity=1)
|
|
269
|
-
self._lock_ = False
|
|
270
|
-
|
|
271
|
-
@property
|
|
272
|
-
def is_occupied(self):
|
|
273
|
-
"""
|
|
274
|
-
判断锁是否被占用
|
|
275
|
-
"""
|
|
276
|
-
return self._occupied_.count > 0
|
|
277
|
-
|
|
278
|
-
def lock(self):
|
|
279
|
-
"""
|
|
280
|
-
请求锁
|
|
281
|
-
"""
|
|
282
|
-
return self._occupied_.request()
|
|
283
|
-
|
|
284
|
-
def unlock(self, req: simpy.resources.resource.Request):
|
|
285
|
-
"""
|
|
286
|
-
请求解锁
|
|
287
|
-
"""
|
|
288
|
-
return self._occupied_.release(req)
|
ipa/func/app.py
DELETED
|
File without changes
|
|
File without changes
|
/ipa/{logger → logging}/fmt.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/ipa/{logger → logging}/util.py
RENAMED
|
File without changes
|
|
File without changes
|