ipa-python-kit 0.1.3.dev1__py3-none-any.whl → 1.0.0__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 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(l: Length, v: Speed):
199
+ def get_time(length: Length, v: Speed):
200
200
  """
201
201
  距离/速度=时间
202
202
  TODO:处理单位
203
203
  """
204
- assert l.value is not None, "length is not specified"
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(l.value / v.value)
207
+ return abs(length.value / v.value)
208
208
 
209
209
 
210
210
  def get_time_3d(delta_distance: Point, v: ThreeDimensionalVelocity):
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
- if pandas and isinstance(date_str, (pandas.Timestamp)):
39
- return date_str.to_pydatetime()
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, Callable, Dict, Iterable, Tuple, Union
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
- def get_the_first_existent_key(obj: dict, *keys, default=None) -> Tuple[Any, Any]:
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 (dict): 输入的字典,键名可能是中文或其他形式。
48
- keys (Iterable[Any]): 要查找的键名列表。
49
- default (Any, optional): 如果没有找到任何键,返回的默认值。默认值为None。
56
+ obj (Dict[K,V]): 输入的字典,键名可能是中文或其他形式。
57
+ keys (Iterable[K]): 要查找的键名列表。
58
+ default (V, optional): 如果没有找到任何键,返回的默认值。默认值为None。
50
59
 
51
60
  Returns:
52
- (Tuple[Any, Any]): 第一个存在的键值对,键为存在的键名,值为对应的值。如果没有找到任何键,返回(None, default)。
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 ..data_type import Point
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
- links.append(opts.GraphLink(source=str(u), target=str(v)))
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=opts.TooltipOpts(trigger="item", formatter="{b}"),
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
- def get_class_full_path(cls) -> str:
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
- str: 类的全路径,格式为 "模块路径.类名"
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
- # __qualname__ 获取类的限定名(含嵌套关系,如 "OuterClass.InnerClass")
19
- return f"{cls.__module__}.{cls.__qualname__}"
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
- base_class: Type[T], _from: ModuleType, include_base_class: bool = False
69
+ parent: Type[T], _from: ModuleType, include_parent: bool = False
24
70
  ):
25
71
  """
26
- 收集指定模块中所有继承自 base_class 的子类
27
-
28
- 参数:
29
- base_class: 基类类型
30
- _from: 要搜索的模块对象
31
- include_base_class: 是否包含基类本身,默认为 False
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(m):
38
- if not include_base_class and m is base_class:
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)
@@ -45,6 +45,7 @@ def rename_keys_by_field_name(m: Type[BaseModel], data: dict):
45
45
  2. 如果有多个别名,以最后一个别名为准
46
46
  """
47
47
  for name, f in m.model_fields.items():
48
+ # TODO: 是否可能因为集合的无序性导致不一致问题
48
49
  aliases = collect_field_aliases(f)
49
50
  for alias in aliases:
50
51
  if alias in data:
ipa/version.py CHANGED
@@ -1,5 +1,6 @@
1
- # file generated by setuptools-scm
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__: VERSION_TUPLE
27
- version_tuple: VERSION_TUPLE
28
- commit_id: COMMIT_ID
29
- __commit_id__: 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.3.dev1'
32
- __version_tuple__ = version_tuple = (0, 1, 3, 'dev1')
21
+ __version__ = version = '1.0.0'
22
+ __version_tuple__ = version_tuple = (1, 0, 0)
33
23
 
34
24
  __commit_id__ = commit_id = None
@@ -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, fastapi.WebSocket, ClientConnection]
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, fastapi.WebSocket):
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, fastapi.WebSocket):
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, fastapi.WebSocket):
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, fastapi.WebSocket):
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.dev1
3
+ Version: 1.0.0
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.8
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,7 +43,15 @@ 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
- # pyproject-tmpl
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
 
@@ -58,16 +66,19 @@ Requires-Dist: simpy>=4.1.1; extra == "des"
58
66
 
59
67
  ## introduction
60
68
 
61
- the project uses `src-layout`, includes `sdk` (for publish) and `biz` (for business logic).
62
- `sdk` is designed for publish and no need to rename, we use file/folder mapping `mylib=src/sdk` in `pyproject.toml`.
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. just replace `mylib` with `[the name you want]` in the whole project
70
- 1. remember to `uv pip install -e .[all]` to make sure `mylib` is available in development.
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
 
@@ -1,5 +1,6 @@
1
1
  ipa/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- ipa/version.py,sha256=QoW52qo3GMLTeZ0fH3Wz2Je9ELIaRJTFNj8XLzgdrok,751
2
+ ipa/version.py,sha256=iNy4L1FSyQ-2tV1aGTwtqfit5nbPuc8UTnEC1of1vuI,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=2zpM8ttViWYH7Po0gEErXvO2rZ5okZHXs7XpLGh8f3A,12598
25
+ ipa/data_type/unitable.py,sha256=pZ8-rEx2h8SLvO7xAaAT52JX2608pwkempPLlBT2h7s,12613
25
26
  ipa/datetime/__init__.py,sha256=nE8jJjGJaKuJKCyuf6QLfrj8xPj8IqGKeH2VXP3FMzY,21
26
- ipa/datetime/core.py,sha256=rKr_G3kNkIm4lC9P-BqaSorb_311hs9rijdfW-kaH7A,1733
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=8kUL-eM84VRK5cBdQ3t9Mw8qug5mf7C5GTIz-YL07Ts,2837
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/logger/__init__.py,sha256=ejMSLTUiIDtCYh69INFx84Byfmse6X2VDtiuWGjICy8,1579
51
- ipa/logger/builder.py,sha256=AnfpSchz3hzMHk8kdsUwCDLa4WzrXFtBxMXjSBsYZaU,5793
52
- ipa/logger/fmt.py,sha256=mvB02qhFY20QOMkownG9MUHsUBKS1j6SWpqS7sHzQVE,649
53
- ipa/logger/handler.py,sha256=d9bifBms4Bn_Iwl71RkBDix6Rm8aQO4pCcBCpATJ3Mo,488
54
- ipa/logger/util.py,sha256=fMnxe_7hnQmY1AtFZs_zwqihMOdS60jbaoCNSi_GUzo,9868
55
- ipa/logger/model/__init__.py,sha256=xngWypB4IA8Y9jR9XfVftRxIeG5DsXQ34xZ5sE8pHxQ,28
56
- ipa/logger/model/dict_config.py,sha256=MlFEv761AOptxv_-s6GOWWcNECmVHpjLb9OfExRDJ4c,1815
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=dhLkiVP-S6--80oCxCg7BT1Q-ut9sXB0sFWUarVxMJE,10455
60
- ipa/pkg/__init__.py,sha256=Oj0wdT3VLX-5exaJpleBJk7aCzfFOGOZsY_rmkGVeo0,1269
61
- ipa/pkg/hot_reload.py,sha256=k2v13u_GQlJ2rwHV_TC2oY0Vablrg8CbthVpGog12c8,935
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=roV1r2q36rmvdJ0TPLllu03LGt6ScxePpmMTiDdDqTc,2036
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=1nSItnQzfZt2bBJ8NGeKEZdAuAhnFloSCr28hZpqgTo,4805
71
- ipa_python_kit-0.1.3.dev1.dist-info/METADATA,sha256=HkJnpok1Snyo_oRBinFdHgxIQj4bqTwY3hXldio_Skc,2671
72
- ipa_python_kit-0.1.3.dev1.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
73
- ipa_python_kit-0.1.3.dev1.dist-info/top_level.txt,sha256=jpjQhUSwP61wKQAysY61KwKKom42vtQTiJNXMT6AyCc,4
74
- ipa_python_kit-0.1.3.dev1.dist-info/RECORD,,
68
+ ipa/websocket/core/__init__.py,sha256=EOqo4MnWXdjHcmqiP61pWIinbhDYb4_Lesiei7uiyXk,4843
69
+ ipa_python_kit-1.0.0.dist-info/METADATA,sha256=K--OWqeANjIPaU_-hmiSS0agwxisSY54GyHoz-W5oLg,2846
70
+ ipa_python_kit-1.0.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
71
+ ipa_python_kit-1.0.0.dist-info/top_level.txt,sha256=jpjQhUSwP61wKQAysY61KwKKom42vtQTiJNXMT6AyCc,4
72
+ ipa_python_kit-1.0.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.0)
2
+ Generator: setuptools (82.0.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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
@@ -1,12 +0,0 @@
1
- from pathlib import Path
2
-
3
-
4
- class AppRoot(Path):
5
- """
6
- 鉴于每个应用都要有自己的logs、data目录,这里索性增加个工具统一管理
7
- """
8
-
9
- def dir_of(self, name: str):
10
- d = self.joinpath(name)
11
- d.mkdir(parents=True, exist_ok=True)
12
- return d
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes