ErisPulse 2.4.2.dev0__py3-none-any.whl → 2.4.2.dev1__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.
ErisPulse/Core/adapter.py CHANGED
@@ -51,6 +51,9 @@ class AdapterManager(ManagerBase):
51
51
  # Bot状态存储 - {platform: {bot_id: {"status": str, "last_active": float, "info": dict}}}
52
52
  self._bots: dict[str, dict[str, dict]] = {}
53
53
 
54
+ # 标记是否正在关闭,避免重复提交离线事件
55
+ self._is_being_shutdown = False
56
+
54
57
  def set_sdk_ref(self, sdk) -> bool:
55
58
  """
56
59
  设置 SDK 引用
@@ -287,127 +290,134 @@ class AdapterManager(ManagerBase):
287
290
  >>> # 关闭多个适配器
288
291
  >>> await adapter.shutdown(["Platform1", "Platform2"])
289
292
  """
290
- if platforms is None:
291
- platforms = list(self._adapters.keys())
292
- if not isinstance(platforms, list):
293
- platforms = [platforms]
294
- for platform in platforms:
295
- if platform not in self._adapters:
296
- raise ValueError(f"平台 {platform} 未注册")
297
-
298
- logger.info(f"关闭适配器 {platforms}")
299
-
300
- # 提交适配器关闭开始事件
301
- await lifecycle.submit_event(
302
- "adapter.stop", msg="开始关闭适配器", data={"platforms": platforms}
303
- )
304
-
305
- from .router import router
306
-
307
- # 需要收集受影响的 adapter 实例(因为多个平台可能共享同一个实例)
308
- affected_adapters = set()
309
- bots_to_offline = [] # [(platform, bot_id), ...]
293
+ # 设置关闭标志,避免重复提交离线事件
294
+ self._is_being_shutdown = True
310
295
 
311
- # 取消目标平台的后台启动任务
312
- for platform in platforms:
313
- task = self._adapter_tasks.pop(platform, None)
314
- if task and not task.done():
315
- task.cancel()
316
- logger.debug(f"已取消平台 {platform} 的后台启动任务")
317
-
318
- for platform in platforms:
319
- adapter_instance = self._adapters[platform]
320
- affected_adapters.add(adapter_instance)
321
-
322
- # 收集该平台下需要标记为离线的 Bot
323
- if platform in self._bots:
324
- for bot_id, bot_info in self._bots[platform].items():
325
- if bot_info.get("status") != "offline":
326
- bots_to_offline.append((platform, bot_id))
296
+ try:
297
+ if platforms is None:
298
+ platforms = list(self._adapters.keys())
299
+ if not isinstance(platforms, list):
300
+ platforms = [platforms]
301
+ for platform in platforms:
302
+ if platform not in self._adapters:
303
+ raise ValueError(f"平台 {platform} 未注册")
327
304
 
328
- # 对每个受影响的 adapter 实例执行 shutdown(如果尚未关闭)
329
- for adapter_instance in affected_adapters:
330
- if adapter_instance in self._started_instances:
331
- # 找到该实例对应的平台名(用于事件提交)
332
- instance_platforms = [
333
- p for p, a in self._adapters.items() if a is adapter_instance
334
- ]
335
- platform_label = (
336
- instance_platforms[0]
337
- if instance_platforms
338
- else str(id(adapter_instance))
339
- )
305
+ logger.info(f"关闭适配器 {platforms}")
340
306
 
341
- # 提交适配器状态变化事件(stopping)
342
- for p in instance_platforms:
343
- if p in platforms:
344
- await lifecycle.submit_event(
345
- "adapter.status.change",
346
- msg=f"适配器 {p} 状态变化: stopping",
347
- data={"platform": p, "status": "stopping"},
348
- )
307
+ # 提交适配器关闭开始事件
308
+ await lifecycle.submit_event(
309
+ "adapter.stop", msg="开始关闭适配器", data={"platforms": platforms}
310
+ )
349
311
 
350
- try:
351
- await adapter_instance.shutdown()
352
- self._started_instances.remove(adapter_instance)
312
+ from .router import router
313
+
314
+ # 需要收集受影响的 adapter 实例(因为多个平台可能共享同一个实例)
315
+ affected_adapters = set()
316
+ bots_to_offline = [] # [(platform, bot_id), ...]
317
+
318
+ # 取消目标平台的后台启动任务
319
+ for platform in platforms:
320
+ task = self._adapter_tasks.pop(platform, None)
321
+ if task and not task.done():
322
+ task.cancel()
323
+ logger.debug(f"已取消平台 {platform} 的后台启动任务")
324
+
325
+ for platform in platforms:
326
+ adapter_instance = self._adapters[platform]
327
+ affected_adapters.add(adapter_instance)
328
+
329
+ # 收集该平台下需要标记为离线的 Bot
330
+ if platform in self._bots:
331
+ for bot_id, bot_info in self._bots[platform].items():
332
+ if bot_info.get("status") != "offline":
333
+ bots_to_offline.append((platform, bot_id))
334
+
335
+ # 对每个受影响的 adapter 实例执行 shutdown(如果尚未关闭)
336
+ for adapter_instance in affected_adapters:
337
+ if adapter_instance in self._started_instances:
338
+ # 找到该实例对应的平台名(用于事件提交)
339
+ instance_platforms = [
340
+ p for p, a in self._adapters.items() if a is adapter_instance
341
+ ]
342
+ platform_label = (
343
+ instance_platforms[0]
344
+ if instance_platforms
345
+ else str(id(adapter_instance))
346
+ )
353
347
 
354
- # 提交适配器状态变化事件(stopped
348
+ # 提交适配器状态变化事件(stopping
355
349
  for p in instance_platforms:
356
350
  if p in platforms:
357
351
  await lifecycle.submit_event(
358
352
  "adapter.status.change",
359
- msg=f"适配器 {p} 状态变化: stopped",
360
- data={"platform": p, "status": "stopped"},
353
+ msg=f"适配器 {p} 状态变化: stopping",
354
+ data={"platform": p, "status": "stopping"},
361
355
  )
362
- except Exception as e:
363
- logger.error(f"关闭适配器实例 {id(adapter_instance)} 失败: {e}")
364
356
 
365
- # 提交适配器状态变化事件(stop_failed)
366
- for p in instance_platforms:
367
- if p in platforms:
368
- await lifecycle.submit_event(
369
- "adapter.status.change",
370
- msg=f"适配器 {p} 状态变化: stop_failed",
371
- data={
372
- "platform": p,
373
- "status": "stop_failed",
374
- "error": str(e),
375
- },
376
- )
357
+ try:
358
+ await adapter_instance.shutdown()
359
+ self._started_instances.remove(adapter_instance)
360
+
361
+ # 提交适配器状态变化事件(stopped)
362
+ for p in instance_platforms:
363
+ if p in platforms:
364
+ await lifecycle.submit_event(
365
+ "adapter.status.change",
366
+ msg=f"适配器 {p} 状态变化: stopped",
367
+ data={"platform": p, "status": "stopped"},
368
+ )
369
+ except Exception as e:
370
+ logger.error(f"关闭适配器实例 {id(adapter_instance)} 失败: {e}")
371
+
372
+ # 提交适配器状态变化事件(stop_failed)
373
+ for p in instance_platforms:
374
+ if p in platforms:
375
+ await lifecycle.submit_event(
376
+ "adapter.status.change",
377
+ msg=f"适配器 {p} 状态变化: stop_failed",
378
+ data={
379
+ "platform": p,
380
+ "status": "stop_failed",
381
+ "error": str(e),
382
+ },
383
+ )
384
+
385
+ # 清理被关闭平台的路由
386
+ for platform in platforms:
387
+ result = router.unregister_all_by_namespace(platform)
388
+ if result["http_count"] > 0 or result["websocket_count"] > 0:
389
+ logger.debug(
390
+ f"已清理平台 {platform} 的路由: HTTP={result['http_count']}, WebSocket={result['websocket_count']}"
391
+ )
377
392
 
378
- # 清理被关闭平台的路由
379
- for platform in platforms:
380
- result = router.unregister_all_by_namespace(platform)
381
- if result["http_count"] > 0 or result["websocket_count"] > 0:
382
- logger.debug(
383
- f"已清理平台 {platform} 的路由: HTTP={result['http_count']}, WebSocket={result['websocket_count']}"
384
- )
393
+ # 停止路由器(仅当所有适配器都关闭时)
394
+ if not self._started_instances:
395
+ await router.stop()
385
396
 
386
- # 停止路由器(仅当所有适配器都关闭时)
387
- if not self._started_instances:
388
- await router.stop()
389
-
390
- # 将相关 Bot 标记为离线
391
- for platform, bot_id in bots_to_offline:
392
- if platform in self._bots and bot_id in self._bots[platform]:
393
- self._bots[platform][bot_id]["status"] = "offline"
394
- # 提交 Bot 离线事件
395
- await lifecycle.submit_event(
396
- "adapter.bot.offline",
397
- msg=f"Bot {platform}/{bot_id} 离线",
398
- data={"platform": platform, "bot_id": bot_id, "status": "offline"},
399
- )
397
+ # 将相关 Bot 标记为离线
398
+ for platform, bot_id in bots_to_offline:
399
+ if platform in self._bots and bot_id in self._bots[platform]:
400
+ self._bots[platform][bot_id]["status"] = "offline"
401
+ # 提交 Bot 离线事件
402
+ await lifecycle.submit_event(
403
+ "adapter.bot.offline",
404
+ msg=f"Bot {platform}/{bot_id} 离线",
405
+ data={"platform": platform, "bot_id": bot_id, "status": "offline"},
406
+ )
400
407
 
401
- # 如果所有适配器都关闭了,清理事件处理器
402
- if not self._started_instances:
403
- self._onebot_handlers.clear()
404
- self._raw_handlers.clear()
405
- self._onebot_middlewares.clear()
408
+ # 如果所有适配器都关闭了,清理事件处理器
409
+ if not self._started_instances:
410
+ self._onebot_handlers.clear()
411
+ self._raw_handlers.clear()
412
+ self._onebot_middlewares.clear()
406
413
 
407
- # 提交适配器关闭完成事件
408
- await lifecycle.submit_event(
409
- "adapter.stopped", msg="适配器关闭完成", data={"platforms": platforms}
410
- )
414
+ # 提交适配器关闭完成事件
415
+ await lifecycle.submit_event(
416
+ "adapter.stopped", msg="适配器关闭完成", data={"platforms": platforms}
417
+ )
418
+ finally:
419
+ # 清除关闭标志
420
+ self._is_being_shutdown = False
411
421
 
412
422
  def clear(self) -> None:
413
423
  """
@@ -473,12 +483,11 @@ class AdapterManager(ManagerBase):
473
483
 
474
484
  def exists(self, platform: str) -> bool:
475
485
  """
476
- 检查平台是否存在
486
+ 检查平台是否已注册
477
487
 
478
488
  :param platform: 平台名称
479
- :return: [bool] 平台是否存在
489
+ :return: 平台是否已注册(即 adapter.register() 已被调用)
480
490
  """
481
- # 检查平台是否已注册(在 _adapters 中)
482
491
  return platform in self._adapters
483
492
 
484
493
  def is_enabled(self, platform: str) -> bool:
@@ -486,20 +495,25 @@ class AdapterManager(ManagerBase):
486
495
  检查平台适配器是否启用
487
496
 
488
497
  :param platform: 平台名称
489
- :return: [bool] 平台适配器是否启用
498
+ :return: 平台适配器是否启用
499
+
500
+ {!--< tips >!--}
501
+ 适配器启用条件:
502
+ 1. 适配器在配置文件中(ErisPulse.adapters.status.{platform} 存在)
503
+ 2. 配置值为启用状态
504
+
505
+ 如果适配器未在配置中,返回 False
506
+ {!--< /tips >!--}
490
507
  """
491
- # 不使用默认值,如果配置不存在则返回 None
508
+ from .config import parse_bool_config
509
+
492
510
  status = config.getConfig(f"ErisPulse.adapters.status.{platform}")
493
511
 
494
- # 如果状态不存在,说明是新适配器
512
+ # 适配器未在配置中,返回 False
495
513
  if status is None:
496
- return False # 新适配器默认不启用,需要在初始化时处理
497
-
498
- # 处理字符串形式的布尔值
499
- if isinstance(status, str):
500
- return status.lower() not in ("false", "0", "no", "off")
514
+ return False
501
515
 
502
- return bool(status)
516
+ return parse_bool_config(status)
503
517
 
504
518
  def enable(self, platform: str) -> bool:
505
519
  """
@@ -541,7 +555,7 @@ class AdapterManager(ManagerBase):
541
555
  :return: 是否取消成功
542
556
 
543
557
  {!--< internal-use >!--}
544
- 注意:此方法仅取消注册,不关闭已启动的适配器
558
+ 注意: 此方法仅取消注册, 不关闭已启动的适配器
545
559
  {!--< /internal-use >!--}
546
560
  """
547
561
  if platform not in self._adapters:
@@ -837,13 +851,15 @@ class AdapterManager(ManagerBase):
837
851
  )
838
852
 
839
853
  if status == "offline":
840
- asyncio.ensure_future(
841
- lifecycle.submit_event(
842
- "adapter.bot.offline",
843
- msg=f"Bot {platform}/{bot_id} 离线",
844
- data={"platform": platform, "bot_id": bot_id, "status": "offline"},
854
+ # 只有在非主动关闭的情况下才提交事件(避免与 shutdown() 重复)
855
+ if not self._is_being_shutdown:
856
+ asyncio.ensure_future(
857
+ lifecycle.submit_event(
858
+ "adapter.bot.offline",
859
+ msg=f"Bot {platform}/{bot_id} 离线",
860
+ data={"platform": platform, "bot_id": bot_id, "status": "offline"},
861
+ )
845
862
  )
846
- )
847
863
 
848
864
  def _update_bot_heartbeat(self, platform: str, self_info: dict) -> None:
849
865
  """
@@ -104,10 +104,10 @@ class AdapterManager(ManagerBase):
104
104
  ...
105
105
  def exists(self: object, platform: str) -> bool:
106
106
  """
107
- 检查平台是否存在
107
+ 检查平台是否已注册
108
108
 
109
109
  :param platform: 平台名称
110
- :return: [bool] 平台是否存在
110
+ :return: 平台是否已注册(即 adapter.register() 已被调用)
111
111
  """
112
112
  ...
113
113
  def is_enabled(self: object, platform: str) -> bool:
@@ -115,7 +115,15 @@ class AdapterManager(ManagerBase):
115
115
  检查平台适配器是否启用
116
116
 
117
117
  :param platform: 平台名称
118
- :return: [bool] 平台适配器是否启用
118
+ :return: 平台适配器是否启用
119
+
120
+ {!--< tips >!--}
121
+ 适配器启用条件:
122
+ 1. 适配器在配置文件中(ErisPulse.adapters.status.{platform} 存在)
123
+ 2. 配置值为启用状态
124
+
125
+ 如果适配器未在配置中,返回 False
126
+ {!--< /tips >!--}
119
127
  """
120
128
  ...
121
129
  def enable(self: object, platform: str) -> bool:
ErisPulse/Core/config.py CHANGED
@@ -24,8 +24,9 @@ class ConfigManager:
24
24
  self._cache_timestamp = 0 # 缓存时间戳
25
25
  self._cache_timeout = 60 # 缓存超时时间(秒)
26
26
  self._write_delay = 5 # 写入延迟(秒)
27
- self._write_timer = None # 写入定时器
27
+ self._write_timer: threading.Timer | None = None # 写入定时器
28
28
  self._lock = threading.RLock() # 线程安全锁
29
+ self._file_lock = threading.RLock() # 文件操作锁,确保原子性
29
30
  self._migrate_config() # 迁移旧配置文件
30
31
  self._load_config() # 初始化时加载配置
31
32
 
@@ -138,56 +139,79 @@ class ConfigManager:
138
139
  def _flush_config(self) -> None:
139
140
  """
140
141
  将待写入的配置刷新到文件
142
+
143
+ 使用文件锁确保多线程环境下的原子性操作
141
144
  """
142
145
  with self._lock:
143
146
  if not self._dirty_keys:
144
147
  return # 没有需要写入的内容
145
148
 
146
- try:
147
- # 从文件读取完整配置
148
- if os.path.exists(self.CONFIG_FILE):
149
- with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
150
- config = toml.load(f)
151
- else:
152
- config = {}
153
-
154
- # 应用待写入的更改
155
- for key, value in self._dirty_keys.items():
156
- keys = key.split(".")
157
- current = config
158
- for k in keys[:-1]:
159
- if k not in current:
160
- current[k] = {}
161
- current = current[k]
162
- current[keys[-1]] = value
163
-
164
- # 对配置字典进行排序,确保同一模块的配置项排列在一起
165
- sorted_config = self._sort_config_dict(config)
166
-
167
- # 写入文件
168
- with open(self.CONFIG_FILE, "w", encoding="utf-8") as f:
169
- toml.dump(sorted_config, f)
170
-
171
- # 更新缓存并清除待写入队列
172
- self._cache = sorted_config
173
- self._cache_timestamp = time.time()
174
- self._dirty_keys.clear()
149
+ with self._file_lock: # 确保文件操作原子性
150
+ try:
151
+ # 从文件读取完整配置
152
+ if os.path.exists(self.CONFIG_FILE):
153
+ with open(self.CONFIG_FILE, "r", encoding="utf-8") as f:
154
+ config = toml.load(f)
155
+ else:
156
+ config = {}
157
+
158
+ # 应用待写入的更改
159
+ for key, value in self._dirty_keys.items():
160
+ keys = key.split(".")
161
+ current = config
162
+ for k in keys[:-1]:
163
+ if k not in current:
164
+ current[k] = {}
165
+ current = current[k]
166
+ current[keys[-1]] = value
167
+
168
+ # 对配置字典进行排序,确保同一模块的配置项排列在一起
169
+ sorted_config = self._sort_config_dict(config)
170
+
171
+ # 写入临时文件,确保原子性
172
+ temp_file = self.CONFIG_FILE + ".tmp"
173
+ with open(temp_file, "w", encoding="utf-8") as f:
174
+ toml.dump(sorted_config, f)
175
+
176
+ # 原子性重命名(跨平台兼容)
177
+ if os.name == 'nt': # Windows
178
+ if os.path.exists(self.CONFIG_FILE):
179
+ os.replace(temp_file, self.CONFIG_FILE)
180
+ else:
181
+ os.rename(temp_file, self.CONFIG_FILE)
182
+ else: # Unix/Linux/macOS
183
+ os.rename(temp_file, self.CONFIG_FILE)
184
+
185
+ # 更新缓存并清除待写入队列
186
+ self._cache = sorted_config
187
+ self._cache_timestamp = time.time()
188
+ self._dirty_keys.clear()
175
189
 
176
- except Exception as e:
177
- from .logger import logger
190
+ except Exception as e:
191
+ from .logger import logger
178
192
 
179
- logger.error(f"写入配置文件 {self.CONFIG_FILE} 失败: {e}")
193
+ logger.error(f"写入配置文件 {self.CONFIG_FILE} 失败: {e}")
194
+ # 清理临时文件
195
+ temp_file = self.CONFIG_FILE + ".tmp"
196
+ if os.path.exists(temp_file):
197
+ try:
198
+ os.remove(temp_file)
199
+ except Exception:
200
+ pass
180
201
 
181
202
  def _schedule_write(self) -> None:
182
203
  """
183
204
  安排延迟写入
205
+
206
+ 线程安全:使用锁保护 Timer 的取消和创建
184
207
  """
185
- if self._write_timer:
186
- self._write_timer.cancel()
208
+ with self._lock:
209
+ if self._write_timer:
210
+ self._write_timer.cancel()
187
211
 
188
- self._write_timer = threading.Timer(self._write_delay, self._flush_config)
189
- self._write_timer.daemon = True
190
- self._write_timer.start()
212
+ self._write_timer = threading.Timer(self._write_delay, self._flush_config)
213
+ self._write_timer.daemon = True
214
+ self._write_timer.start()
191
215
 
192
216
  def _check_cache_validity(self) -> None:
193
217
  """
@@ -268,4 +292,30 @@ class ConfigManager:
268
292
 
269
293
  config: ConfigManager = ConfigManager()
270
294
 
271
- __all__ = ["config"]
295
+
296
+ def parse_bool_config(value: Any) -> bool:
297
+ """
298
+ 解析配置中的布尔值
299
+
300
+ :param value: 配置值(可以是 bool, int, str 等)
301
+ :return: 解析后的布尔值
302
+
303
+ 支持的值:
304
+ - True: True, 1, "true", "True", "1", "yes", "Yes", "on", "On"
305
+ - False: False, 0, "false", "False", "0", "no", "No", "off", "Off"
306
+ """
307
+ if isinstance(value, bool):
308
+ return value
309
+
310
+ if isinstance(value, int):
311
+ return value != 0
312
+
313
+ if isinstance(value, str):
314
+ normalized = value.lower().strip()
315
+ return normalized in ("true", "1", "yes", "on")
316
+
317
+ # 其他类型尝试转换为布尔值
318
+ return bool(value)
319
+
320
+
321
+ __all__ = ["config", "parse_bool_config"]
ErisPulse/Core/config.pyi CHANGED
@@ -46,11 +46,15 @@ class ConfigManager:
46
46
  def _flush_config(self: object) -> None:
47
47
  """
48
48
  将待写入的配置刷新到文件
49
+
50
+ 使用文件锁确保多线程环境下的原子性操作
49
51
  """
50
52
  ...
51
53
  def _schedule_write(self: object) -> None:
52
54
  """
53
55
  安排延迟写入
56
+
57
+ 线程安全:使用锁保护 Timer 的取消和创建
54
58
  """
55
59
  ...
56
60
  def _check_cache_validity(self: object) -> None:
@@ -87,3 +91,16 @@ class ConfigManager:
87
91
  ...
88
92
 
89
93
  config: ConfigManager
94
+
95
+ def parse_bool_config(value: Any) -> bool:
96
+ """
97
+ 解析配置中的布尔值
98
+
99
+ :param value: 配置值(可以是 bool, int, str 等)
100
+ :return: 解析后的布尔值
101
+
102
+ 支持的值:
103
+ - True: True, 1, "true", "True", "1", "yes", "Yes", "on", "On"
104
+ - False: False, 0, "false", "False", "0", "no", "No", "off", "Off"
105
+ """
106
+ ...
ErisPulse/Core/module.py CHANGED
@@ -272,15 +272,17 @@ class ModuleManager(ManagerBase):
272
272
 
273
273
  def exists(self, module_name: str) -> bool:
274
274
  """
275
- 检查模块是否存在(已注册或在配置中)
275
+ 检查模块是否已注册
276
276
 
277
- :param module_name: [str] 模块名称
278
- :return: [bool] 模块是否存在
277
+ :param module_name: 模块名称
278
+ :return: 模块是否已注册(即 module.register() 已被调用)
279
+
280
+ {!--< tips >!--}
281
+ exists() 只检查模块类是否已注册到管理器,用于验证模块是否可以加载。
282
+ 如需检查模块是否启用,请使用 is_enabled()。
283
+ {!--< /tips >!--}
279
284
  """
280
- if module_name in self._module_classes:
281
- return True
282
- module_statuses = config.getConfig("ErisPulse.modules.status", {})
283
- return module_name in module_statuses
285
+ return module_name in self._module_classes
284
286
 
285
287
  def is_loaded(self, module_name: str) -> bool:
286
288
  """
@@ -350,9 +352,9 @@ class ModuleManager(ManagerBase):
350
352
  {!--< internal-use >!--}
351
353
  此方法仅供内部使用
352
354
 
353
- :param module_name: [str] 模块名称
354
- :param enabled: [bool] 是否启用模块 (默认: False)
355
- :return: [bool] 操作是否成功
355
+ :param module_name: 模块名称
356
+ :param enabled: 是否启用模块 (默认: False)
357
+ :return: 操作是否成功
356
358
  """
357
359
  if self.exists(module_name):
358
360
  return True
@@ -367,18 +369,27 @@ class ModuleManager(ManagerBase):
367
369
  """
368
370
  检查模块是否启用
369
371
 
370
- :param module_name: [str] 模块名称
371
- :return: [bool] 模块是否启用
372
+ :param module_name: 模块名称
373
+ :return: 模块是否启用
374
+
375
+ {!--< tips >!--}
376
+ 模块启用条件:
377
+ 1. 模块在配置文件中(ErisPulse.modules.status.{module_name} 存在)
378
+ 2. 配置值为启用状态
379
+
380
+ 如果模块未在配置中,返回 False
381
+ {!--< /tips >!--}
372
382
  """
373
- if (
374
- status := config.getConfig(f"ErisPulse.modules.status.{module_name}")
375
- ) is None:
376
- return False
383
+ from .config import parse_bool_config
384
+
385
+ status = config.getConfig(f"ErisPulse.modules.status.{module_name}")
377
386
 
378
- if isinstance(status, str):
379
- return status.lower() not in ("false", "0", "no", "off")
387
+ # 模块未在配置中,返回 False
388
+ if status is None:
389
+ return False
380
390
 
381
- return bool(status)
391
+ # 解析配置值
392
+ return parse_bool_config(status)
382
393
 
383
394
  def enable(self, module_name: str) -> bool:
384
395
  """
ErisPulse/Core/module.pyi CHANGED
@@ -91,10 +91,15 @@ class ModuleManager(ManagerBase):
91
91
  ...
92
92
  def exists(self: object, module_name: str) -> bool:
93
93
  """
94
- 检查模块是否存在(已注册或在配置中)
94
+ 检查模块是否已注册
95
95
 
96
- :param module_name: [str] 模块名称
97
- :return: [bool] 模块是否存在
96
+ :param module_name: 模块名称
97
+ :return: 模块是否已注册(即 module.register() 已被调用)
98
+
99
+ {!--< tips >!--}
100
+ exists() 只检查模块类是否已注册到管理器,用于验证模块是否可以加载。
101
+ 如需检查模块是否启用,请使用 is_enabled()。
102
+ {!--< /tips >!--}
98
103
  """
99
104
  ...
100
105
  def is_loaded(self: object, module_name: str) -> bool:
@@ -155,8 +160,16 @@ class ModuleManager(ManagerBase):
155
160
  """
156
161
  检查模块是否启用
157
162
 
158
- :param module_name: [str] 模块名称
159
- :return: [bool] 模块是否启用
163
+ :param module_name: 模块名称
164
+ :return: 模块是否启用
165
+
166
+ {!--< tips >!--}
167
+ 模块启用条件:
168
+ 1. 模块在配置文件中(ErisPulse.modules.status.{module_name} 存在)
169
+ 2. 配置值为启用状态
170
+
171
+ 如果模块未在配置中,返回 False
172
+ {!--< /tips >!--}
160
173
  """
161
174
  ...
162
175
  def enable(self: object, module_name: str) -> bool:
ErisPulse/Core/router.py CHANGED
@@ -485,9 +485,14 @@ class RouterManager:
485
485
  self._server_task.cancel()
486
486
  try:
487
487
  await asyncio.wait_for(self._server_task, timeout=5.0)
488
- except (asyncio.CancelledError, asyncio.TimeoutError, Exception):
489
- logger.info("路由服务器已停止")
490
- self._server_task = None
488
+ except asyncio.CancelledError:
489
+ logger.info("路由服务器已被取消")
490
+ except asyncio.TimeoutError:
491
+ logger.warning("路由服务器停止超时,强制终止")
492
+ except Exception as e:
493
+ logger.error(f"路由服务器停止时发生错误: {e}", exc_info=True)
494
+ finally:
495
+ self._server_task = None
491
496
 
492
497
  # 清理所有注册的路由
493
498
  logger.debug("清理所有注册的路由...")
@@ -549,11 +549,15 @@ class LazyModule:
549
549
  """
550
550
  确保模块已初始化
551
551
 
552
- :raises RuntimeError: 当模块需要异步初始化时抛出
553
-
554
552
  {!--< internal-use >!--}
555
553
  内部方法,检查并确保模块已初始化
556
554
  {!--< /internal-use >!--}
555
+
556
+ 设计说明:
557
+ - 支持同步/异步透明的懒加载机制,用户无需感知差异
558
+ - BaseModule 在同步上下文中使用 asyncio.run() 确保初始化完成
559
+ - 非 BaseModule 保持原有逻辑,支持同步初始化
560
+ {!--< internal-use >!--}
557
561
  """
558
562
  if not object.__getattribute__(self, "_initialized"):
559
563
  try:
@@ -561,8 +565,12 @@ class LazyModule:
561
565
 
562
566
  if object.__getattribute__(self, "_is_base_module"):
563
567
  # BaseModule 必须通过 manager.load() 异步初始化
564
- # 同步创建实例后调度异步 on_load 会导致双重实例化
565
- loop.create_task(self._initialize())
568
+ # 在同步上下文中,使用 asyncio.run() 确保初始化完成
569
+ # 在异步上下文中,使用 loop.create_task() 避免阻塞
570
+ if loop.is_running():
571
+ loop.create_task(self._initialize())
572
+ else:
573
+ asyncio.run(self._initialize())
566
574
  return
567
575
 
568
576
  init_method = getattr(
ErisPulse/sdk.py CHANGED
@@ -144,8 +144,17 @@ class SDK:
144
144
  return object.__getattribute__(self, name)
145
145
  except AttributeError:
146
146
  from .Core.logger import logger as _logger
147
- _logger.error(f"[SDK] 未找到属性 {name}, 您可能使用了错误的SDK注册对象")
148
- raise AttributeError
147
+
148
+ # 区分不同场景,提供更准确的错误提示
149
+ if not name.startswith('_'):
150
+ if name in self.module._module_classes:
151
+ _logger.error(f"[SDK] 模块 '{name}' 已注册但未加载或未启用,请检查模块配置")
152
+ elif name in self.adapter._adapters:
153
+ _logger.error(f"[SDK] 适配器 '{name}' 已注册但未启用,请检查适配器配置")
154
+ else:
155
+ _logger.error(f"[SDK] 未找到属性或模块/适配器 '{name}',请检查名称是否正确")
156
+
157
+ raise AttributeError(f"ErisPulse SDK has no attribute '{name}'")
149
158
 
150
159
  def __repr__(self) -> str:
151
160
  """
@@ -363,55 +372,37 @@ class SDK:
363
372
  loaded_modules = module_manager.list_loaded()
364
373
  if loaded_modules:
365
374
  await module_manager.unload()
366
-
375
+
367
376
  # 3. 收集 SDK 对象上的模块属性(在 clear 之前)
368
377
  instance_dict = object.__getattribute__(self._sdk, '__dict__')
369
378
  module_properties_to_clear = set()
370
-
379
+
371
380
  # 收集已加载模块的属性名
372
381
  for module_name in loaded_modules:
373
382
  if module_name in instance_dict:
374
383
  module_properties_to_clear.add(module_name)
375
-
376
- # 收集所有 LazyModule 代理的属性名(包括从未被访问过的)
384
+
385
+ # 处理已初始化的 LazyModule(已访问过,有实例)
377
386
  for attr_name, attr_value in list(instance_dict.items()):
378
387
  if attr_name.startswith('_'):
379
388
  continue
380
389
  from .loaders.module import LazyModule
381
390
  if isinstance(attr_value, LazyModule):
382
- lm_name = object.__getattribute__(attr_value, '_module_name')
383
- if lm_name not in module_properties_to_clear:
384
- # LazyModule 从未被触发初始化,需要手动调用 on_unload
385
- try:
386
- lm_class = object.__getattribute__(attr_value, '_module_class')
387
- lm_manager = object.__getattribute__(attr_value, '_manager_instance')
388
- lm_info = object.__getattribute__(attr_value, '_module_info')
389
- lm_initialized = object.__getattribute__(attr_value, '_initialized')
390
- lm_is_base = object.__getattribute__(attr_value, '_is_base_module')
391
-
392
- if lm_is_base and not lm_initialized:
391
+ # 只处理已初始化的 LazyModule
392
+ lm_initialized = object.__getattribute__(attr_value, '_initialized')
393
+ if lm_initialized:
394
+ lm_name = object.__getattribute__(attr_value, '_module_name')
395
+ instance = object.__getattribute__(attr_value, '_instance')
396
+ if hasattr(instance, 'on_unload'):
397
+ try:
393
398
  import inspect
394
- try:
395
- if lm_manager._sdk is None:
396
- lm_sdk = self._sdk
397
- else:
398
- lm_sdk = lm_manager._sdk
399
-
400
- params = [p for p in inspect.signature(lm_class.__init__).parameters.values() if p.name != "self"]
401
- tmp_instance = lm_class(lm_sdk) if params else lm_class()
402
- if tmp_instance is None:
403
- raise ValueError(f"模块 {lm_name} __init__ 返回 None")
404
- if hasattr(tmp_instance, 'on_unload'):
405
- if inspect.iscoroutinefunction(tmp_instance.on_unload):
406
- await tmp_instance.on_unload({"module_name": lm_name})
407
- else:
408
- tmp_instance.on_unload({"module_name": lm_name})
409
- except Exception as e:
410
- logger.warning(f"清理未初始化懒加载模块 {lm_name} 的 on_unload 失败: {e}")
411
- except Exception as e:
412
- logger.warning(f"清理未初始化懒加载模块 {lm_name} 失败: {e}")
413
-
414
- module_properties_to_clear.add(attr_name)
399
+ if inspect.iscoroutinefunction(instance.on_unload):
400
+ await instance.on_unload({"module_name": lm_name})
401
+ else:
402
+ instance.on_unload({"module_name": lm_name})
403
+ except Exception as e:
404
+ logger.warning(f"清理懒加载模块 {lm_name} 的 on_unload 失败: {e}")
405
+ module_properties_to_clear.add(attr_name)
415
406
 
416
407
  # 4. 清理所有事件处理器
417
408
  Event._clear_all_handlers()
@@ -460,10 +451,13 @@ class SDK:
460
451
  "module_properties_to_clear": list(module_properties_to_clear),
461
452
  },
462
453
  )
463
-
464
- # 9. 清理生命周期事件处理器(在所有事件提交之后)
454
+
455
+ # 等待一小段时间,确保事件处理完成
456
+ await asyncio.sleep(0.1)
457
+
458
+ # 9. 清理生命周期事件处理器(在所有事件完成之后)
465
459
  lifecycle._handlers.clear()
466
-
460
+
467
461
  logger.info(f"SDK反初始化成功 (耗时: {duration_str})")
468
462
  return True
469
463
 
@@ -478,6 +472,13 @@ class SDK:
478
472
  "error": str(e),
479
473
  },
480
474
  )
475
+
476
+ # 等待一小段时间,确保事件处理完成
477
+ await asyncio.sleep(0.1)
478
+
479
+ # 清理生命周期事件处理器(即使在失败时也要清理)
480
+ lifecycle._handlers.clear()
481
+
481
482
  if "attached to a different loop" in str(e):
482
483
  # 这是一个常见的错误,通常是由于SDK在另一个事件循环中运行而导致的。
483
484
  # 在这种情况下,我们直接返回True即可
@@ -734,20 +735,51 @@ if __name__ == "__main__":
734
735
  async def restart(self) -> bool:
735
736
  """
736
737
  SDK 重新启动
737
-
738
- 执行完整的反初始化后再初始化过程,并重新启动适配器
739
-
740
- {!--< tips >!--}
741
- 使用 asyncio.ensure_future 将重启任务注册到事件循环调度器,
742
- 与调用栈完全解耦,确保即使调用方被取消,重启流程也能完整执行。
743
738
 
744
- 注意:设计上就是如此,不需要进行更改 | 针对场景:事件内的模块进行ErisPulse的restart调用
739
+ 执行完整的反初始化后再初始化过程,并重新启动适配器。
740
+
741
+ {!--< tips >!--}
742
+ **重要设计说明**:
743
+
744
+ 此方法使用 `asyncio.ensure_future()` 将重启任务注册到事件循环调度器,
745
+ 与调用栈完全解耦。这是有意为之的设计,原因如下:
746
+
747
+ 1. **事件链路保护**:如果模块在事件处理器内部调用 `restart()`,而重启过程
748
+ 是同步等待的,那么重启会中断当前事件链路,导致事件处理不完整。
749
+
750
+ 2. **后台执行**:重启是一个耗时操作(需要关闭适配器、卸载模块、重新加载),
751
+ 使用 `ensure_future` 可以让它在后台执行,不阻塞调用者。
752
+
753
+ 3. **返回值语义**:方法立即返回 `True` 表示"重启任务已成功调度",
754
+ 而不是"重启已完成"。实际的重启过程在后台进行。
755
+
756
+ **使用场景示例**:
757
+
758
+ >>> # 场景1: 在模块的事件处理器中调用重启
759
+ >>> @Event.on("message")
760
+ >>> async def handle_reload_command(event):
761
+ >>> if event["message"] == "/reload":
762
+ >>> # 使用 ensure_future 确保事件链路不被中断
763
+ >>> await sdk.restart() # ✅ 正确
764
+ >>> # 不要使用 await sdk.restart(),这会导致事件链路中断
765
+ >>>
766
+ >>> # 场景2: 等待重启完成
767
+ >>> # 如果需要等待重启完成,可以使用生命周期事件监听
768
+ >>> @lifecycle.on("core.init.complete")
769
+ >>> async def on_restart_complete(event):
770
+ >>> if event["data"]["success"]:
771
+ >>> logger.info("重启成功!")
772
+ >>>
773
+ >>> # 场景3: 命令触发重启
774
+ >>> @command("restart")
775
+ >>> async def restart_command():
776
+ >>> logger.info("正在重启 SDK...")
777
+ >>> await sdk.restart()
778
+ >>> logger.info("重启任务已调度,将在后台执行")
745
779
  {!--< /tips >!--}
746
-
747
- :return: bool 重新加载是否成功
748
-
749
- :raises RuntimeError: 当初始化失败时抛出
750
-
780
+
781
+ :return: bool 重启任务是否成功调度(并非重启是否完成)
782
+
751
783
  :example:
752
784
  >>> await sdk.restart()
753
785
  """
ErisPulse/sdk.pyi CHANGED
@@ -141,18 +141,49 @@ class SDK:
141
141
  """
142
142
  SDK 重新启动
143
143
 
144
- 执行完整的反初始化后再初始化过程,并重新启动适配器
144
+ 执行完整的反初始化后再初始化过程,并重新启动适配器。
145
145
 
146
146
  {!--< tips >!--}
147
- 使用 asyncio.ensure_future 将重启任务注册到事件循环调度器,
148
- 与调用栈完全解耦,确保即使调用方被取消,重启流程也能完整执行。
149
-
150
- 注意:设计上就是如此,不需要进行更改 | 针对场景:事件内的模块进行ErisPulse的restart调用
147
+ **重要设计说明**:
148
+
149
+ 此方法使用 `asyncio.ensure_future()` 将重启任务注册到事件循环调度器,
150
+ 与调用栈完全解耦。这是有意为之的设计,原因如下:
151
+
152
+ 1. **事件链路保护**:如果模块在事件处理器内部调用 `restart()`,而重启过程
153
+ 是同步等待的,那么重启会中断当前事件链路,导致事件处理不完整。
154
+
155
+ 2. **后台执行**:重启是一个耗时操作(需要关闭适配器、卸载模块、重新加载),
156
+ 使用 `ensure_future` 可以让它在后台执行,不阻塞调用者。
157
+
158
+ 3. **返回值语义**:方法立即返回 `True` 表示"重启任务已成功调度",
159
+ 而不是"重启已完成"。实际的重启过程在后台进行。
160
+
161
+ **使用场景示例**:
162
+
163
+ >>> # 场景1: 在模块的事件处理器中调用重启
164
+ >>> @Event.on("message")
165
+ >>> async def handle_reload_command(event):
166
+ >>> if event["message"] == "/reload":
167
+ >>> # 使用 ensure_future 确保事件链路不被中断
168
+ >>> await sdk.restart() # ✅ 正确
169
+ >>> # 不要使用 await sdk.restart(),这会导致事件链路中断
170
+ >>>
171
+ >>> # 场景2: 等待重启完成
172
+ >>> # 如果需要等待重启完成,可以使用生命周期事件监听
173
+ >>> @lifecycle.on("core.init.complete")
174
+ >>> async def on_restart_complete(event):
175
+ >>> if event["data"]["success"]:
176
+ >>> logger.info("重启成功!")
177
+ >>>
178
+ >>> # 场景3: 命令触发重启
179
+ >>> @command("restart")
180
+ >>> async def restart_command():
181
+ >>> logger.info("正在重启 SDK...")
182
+ >>> await sdk.restart()
183
+ >>> logger.info("重启任务已调度,将在后台执行")
151
184
  {!--< /tips >!--}
152
185
 
153
- :return: bool 重新加载是否成功
154
-
155
- :raises RuntimeError: 当初始化失败时抛出
186
+ :return: bool 重启任务是否成功调度(并非重启是否完成)
156
187
 
157
188
  :example:
158
189
  >>> await sdk.restart()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErisPulse
3
- Version: 2.4.2.dev0
3
+ Version: 2.4.2.dev1
4
4
  Summary: ErisPulse 是一个模块化、可扩展的异步 Python SDK 框架,主要用于构建高效、可维护的机器人应用程序。
5
5
  Author-email: ErisDev <erisdev@88.com>
6
6
  Maintainer-email: "艾莉丝·格雷拉特(WSu2059)" <wsu2059@qq.com>
@@ -2,8 +2,8 @@ ErisPulse/__init__.py,sha256=rIEPa-80J1wlolAE5IavBEAkKh3S5EGpW4TTlbQmDEM,1504
2
2
  ErisPulse/__init__.pyi,sha256=P5aOKFk9OttVVNQFqENMxGT4A4DneWa6BpVls4ktUMw,656
3
3
  ErisPulse/__main__.py,sha256=7tGYe9fBPUWFYH0wZEnk5M2OhcQI6SvbUTo9HvXNsdk,359
4
4
  ErisPulse/__main__.pyi,sha256=ShpCRAsyNREan4WzZUe5ImB5az453DokFzZrcnVM2LM,431
5
- ErisPulse/sdk.py,sha256=4xrdGe0spMcEcWZyxAHmoNb3ZHYb-4PzfTSkDc-fHwU,28584
6
- ErisPulse/sdk.pyi,sha256=ch1HhmvAXbUr0pY5Ydk6SsJxkXK4O6N7yZAkzHSIfHk,5061
5
+ ErisPulse/sdk.py,sha256=7bYuAcC8FC-QcQiTSW6s3Yh4UYPTg4nvz2FTdza7dQ4,29419
6
+ ErisPulse/sdk.pyi,sha256=WT9Rbo9alhO4ztu1k2_X_RktwmE7UGHvVPozxuRrntA,6641
7
7
  ErisPulse/CLI/__init__.py,sha256=LcGh1t4r_YBt0IPrTI9WdsOcWb3hIhGvLHaX4Tl0-QI,94
8
8
  ErisPulse/CLI/__init__.pyi,sha256=MwfRkbzA9MSDQpVl5HYnLq-BN2snw1LfM2asW7-K5xg,192
9
9
  ErisPulse/CLI/base.py,sha256=G3IXDGlsbILoEze0mJcjvvjWDcIxYsKgPJ-t8CwGgDk,1103
@@ -38,17 +38,17 @@ ErisPulse/CLI/utils/package_manager.py,sha256=Sc4UwP4Rbzye9g3lL05kzo5AsfJ_pHFQOl
38
38
  ErisPulse/CLI/utils/package_manager.pyi,sha256=rinvIBxjR5gn3bhCEJ6K0iLyrCXYo2ttMmtJulDHXiM,8147
39
39
  ErisPulse/Core/__init__.py,sha256=eFY-q8j8fiN9_ie-syvglcy_tPQ5qrIufPC2n7LZXpc,1958
40
40
  ErisPulse/Core/__init__.pyi,sha256=8ADqzF23pS8uGZLpRu2dGOjhHsl7WaI0kLm0TvF3PXY,736
41
- ErisPulse/Core/adapter.py,sha256=YrsTbBOV23PVWeffeJSgazCK6ybU2E8Mv6TBLNlxeIw,41657
42
- ErisPulse/Core/adapter.pyi,sha256=J1n950M4oT0btziG43PM2JaXGmAezmNUadsT6n3NP6c,12563
43
- ErisPulse/Core/config.py,sha256=ZmtutKXVv4RP4CRud2bTUrVI4mjHCVOE-0KTEX8FdcQ,8717
44
- ErisPulse/Core/config.pyi,sha256=Wu41o9MpglvVKgQQX-QT2SzsLGTOQB3MDD3tYwLYsnQ,2579
41
+ ErisPulse/Core/adapter.py,sha256=w4HmnBmhlM4E3hpTWFi7MXboISk8tVPWUwB7E82ofBU,42572
42
+ ErisPulse/Core/adapter.pyi,sha256=YQz5JXZ-76JFG3xUhacSWVUc11ChY7GYZoFnSnSrkPM,12876
43
+ ErisPulse/Core/config.py,sha256=ylv9F4sVhYOsI_x1bVbI0rLWw4K6bE08-JAjNMLQlws,10699
44
+ ErisPulse/Core/config.pyi,sha256=hMPispd42iVB-_0rJbtzo3I_wvOJ7VKdysjsB6XZOH0,3092
45
45
  ErisPulse/Core/lifecycle.py,sha256=lJOGL2AI8UkEksX5tQ4hJRm3pT5_IBCVs5AyYUtNv-I,6444
46
46
  ErisPulse/Core/lifecycle.pyi,sha256=nvsgCPy5179OW_J8__7cxX_OPBAJcGqKEC36sIvK0RU,2678
47
47
  ErisPulse/Core/logger.py,sha256=rrof9Iaap9EwX56NfqKqvyKzErmlxlcl9EbgJ9uehRM,15522
48
48
  ErisPulse/Core/logger.pyi,sha256=bobxpD0abXiwC8FD4RaPxGYBa7H2vEY7oNMwJdtAQAM,9117
49
- ErisPulse/Core/module.py,sha256=2vMQuG8yFNcPNsg2qqRNFz88FuQvvlIDc_qeD7kgdFI,18197
50
- ErisPulse/Core/module.pyi,sha256=fMGvzhCl6y-SYGnjpranL6McSCWmZTNrnESuyuHoo7M,7021
51
- ErisPulse/Core/router.py,sha256=5olQLEszKbdTFIaYxh4VXQ0obbgnI9eF1Yx6Lae0E-U,18440
49
+ ErisPulse/Core/module.py,sha256=nSSB2-MUIFIYZpJXnl1oAKYvBFUw54NQVS50F14UM2U,18524
50
+ ErisPulse/Core/module.pyi,sha256=OxFR_y8hcLeMY5c4sRikri0zYd1wApG_1no_UtxhL_Q,7516
51
+ ErisPulse/Core/router.py,sha256=RW8bDVzzD_AJ8BQUeCSqTjODuqFH3AADH6QhIKzOtCw,18675
52
52
  ErisPulse/Core/router.pyi,sha256=zU0Y8uRzU8t_us_zA6S0Yd1_0dqk1CV9V3YsjhuC07c,5316
53
53
  ErisPulse/Core/storage.py,sha256=f2W_yJHONEwHTDYIHYdXiJZj-LDEIbnv8Was1JWyACA,17478
54
54
  ErisPulse/Core/storage.pyi,sha256=1MHAVo-luTn8Bn3I2mMdHBFN6fbf9HXTxNgQnoFiAgE,6178
@@ -94,7 +94,7 @@ ErisPulse/loaders/__init__.py,sha256=LGNzCHnCRKDgIYtWjCbsk2hZ020TKner48dGCIlDQXM
94
94
  ErisPulse/loaders/__init__.pyi,sha256=rwvQW2XJTRR5woGsoOyOFgQEdjEfwye6QZARj0Q8QbY,424
95
95
  ErisPulse/loaders/adapter.py,sha256=Ai-78zXTdpE-xLBdgNngL8C5F3XpJ96QtbCY4YaiTBc,8336
96
96
  ErisPulse/loaders/adapter.pyi,sha256=RBmlw1CG-g9FowLWC8QfHah18WOJMEIGfGRuhfAeAiE,3062
97
- ErisPulse/loaders/module.py,sha256=JJl7kbZ9iiKKtyhRcd0lGXv24yNNxAEi2dZTRKbLQZk,27850
97
+ ErisPulse/loaders/module.py,sha256=npTZ4EH5rZNguaj9CRhmpK8mItGgZRzfa6sQJE1BX9E,28292
98
98
  ErisPulse/loaders/module.pyi,sha256=UUxcQx7fa_-FgtE_WB1MtpvYWb9L0qWwFdxH2X6ozoQ,6020
99
99
  ErisPulse/loaders/strategy.py,sha256=T7pPKlsc4RHs5dbg_JDU-PjIa0x4w7hYlN8ihH3rW8Y,3343
100
100
  ErisPulse/loaders/strategy.pyi,sha256=jVEAgOeR1zNXdX60fHQcCQECgQDYP-J564mrkBBfFgQ,2409
@@ -110,8 +110,8 @@ ErisPulse/runtime/exceptions.py,sha256=ODQuetLnWUdq4qHgk4b-SxgGri-GrgYs9sALCB_qu
110
110
  ErisPulse/runtime/exceptions.pyi,sha256=I9N3S4sNYMjdqVfUCOG6Jwq0nbMHh6biHh7BfY6Ts1o,1588
111
111
  ErisPulse/runtime/frame_config.py,sha256=GN4-e72KU9SVIuXQCDFHZJR8HWXHb5nxzvtQk8ezOsc,5573
112
112
  ErisPulse/runtime/frame_config.pyi,sha256=x_lrPZAfl9hHDs_9NUTom430SaDLcevpjIkomssiJ78,2011
113
- erispulse-2.4.2.dev0.dist-info/METADATA,sha256=KSdZqqf9rCrujGpAVR0olcv02fKngwG0ibkYuz-YHvY,10625
114
- erispulse-2.4.2.dev0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
115
- erispulse-2.4.2.dev0.dist-info/entry_points.txt,sha256=NiOwT6-XQ7KIH1r6J8odjRO-uaKHfr_Vz_UIG96EWXg,187
116
- erispulse-2.4.2.dev0.dist-info/licenses/LICENSE,sha256=c2XbbDpZFu8YVuWV5mBZnGxE1IlYyelE1qW9TJADHV4,1071
117
- erispulse-2.4.2.dev0.dist-info/RECORD,,
113
+ erispulse-2.4.2.dev1.dist-info/METADATA,sha256=PDkmHxqAb45pGhwHbC9P8H0iWiSwb22WdMvyT4gHBkw,10625
114
+ erispulse-2.4.2.dev1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
115
+ erispulse-2.4.2.dev1.dist-info/entry_points.txt,sha256=NiOwT6-XQ7KIH1r6J8odjRO-uaKHfr_Vz_UIG96EWXg,187
116
+ erispulse-2.4.2.dev1.dist-info/licenses/LICENSE,sha256=c2XbbDpZFu8YVuWV5mBZnGxE1IlYyelE1qW9TJADHV4,1071
117
+ erispulse-2.4.2.dev1.dist-info/RECORD,,