hyper-bot 0.75__tar.gz → 0.78.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. {hyper-bot-0.75 → hyper-bot-0.78.2}/Hyper/Adapters/OneBot.py +60 -55
  2. {hyper-bot-0.75 → hyper-bot-0.78.2}/Hyper/Adapters/OneBotLib/Manager.py +46 -10
  3. hyper-bot-0.78.2/Hyper/Adapters/OneBotLib/Res.py +111 -0
  4. hyper-bot-0.78.2/Hyper/Adapters/Satori.py +105 -0
  5. {hyper-bot-0.75 → hyper-bot-0.78.2}/Hyper/Configurator.py +38 -3
  6. {hyper-bot-0.75 → hyper-bot-0.78.2}/Hyper/Events.py +60 -5
  7. hyper-bot-0.78.2/Hyper/Listener.py +17 -0
  8. {hyper-bot-0.75 → hyper-bot-0.78.2}/Hyper/Logger.py +1 -2
  9. {hyper-bot-0.75 → hyper-bot-0.78.2}/Hyper/Manager.py +2 -0
  10. hyper-bot-0.78.2/Hyper/Network.py +155 -0
  11. {hyper-bot-0.75 → hyper-bot-0.78.2}/Hyper/Segments.py +33 -93
  12. {hyper-bot-0.75/Hyper → hyper-bot-0.78.2/Hyper/Utils}/Errors.py +6 -0
  13. {hyper-bot-0.75/Hyper → hyper-bot-0.78.2/Hyper/Utils}/Logic.py +40 -27
  14. hyper-bot-0.78.2/Hyper/Utils/TypeExt.py +191 -0
  15. hyper-bot-0.78.2/Hyper/__init__.py +1 -0
  16. {hyper-bot-0.75 → hyper-bot-0.78.2}/PKG-INFO +1 -1
  17. {hyper-bot-0.75 → hyper-bot-0.78.2}/hyper_bot.egg-info/PKG-INFO +1 -1
  18. {hyper-bot-0.75 → hyper-bot-0.78.2}/hyper_bot.egg-info/SOURCES.txt +6 -4
  19. {hyper-bot-0.75 → hyper-bot-0.78.2}/setup.py +4 -2
  20. hyper-bot-0.75/Hyper/DataBase.py +0 -81
  21. hyper-bot-0.75/Hyper/Listener.py +0 -7
  22. hyper-bot-0.75/Hyper/ModuleClass.py +0 -67
  23. hyper-bot-0.75/Hyper/Network.py +0 -77
  24. {hyper-bot-0.75 → hyper-bot-0.78.2}/LICENSE +0 -0
  25. {hyper-bot-0.75 → hyper-bot-0.78.2}/hyper_bot.egg-info/dependency_links.txt +0 -0
  26. {hyper-bot-0.75 → hyper-bot-0.78.2}/hyper_bot.egg-info/top_level.txt +0 -0
  27. {hyper-bot-0.75 → hyper-bot-0.78.2}/setup.cfg +0 -0
@@ -1,15 +1,15 @@
1
1
  import json
2
- import queue
3
2
  import threading
4
3
  import time
5
4
  import asyncio
6
5
  import os
7
6
  from typing import Union
8
7
 
9
- from Hyper import Configurator, Errors, Logger, Logic, Manager, Network, Events
8
+ from Hyper import Network, Events
9
+ from Hyper.Utils import Errors, Logic
10
+ from Hyper.Manager import reports
10
11
  from Hyper.Events import *
11
12
 
12
- reports = queue.Queue()
13
13
  config = Configurator.cm.get_cfg()
14
14
  logger = Logger.Logger()
15
15
  logger.set_level(config.log_level)
@@ -24,20 +24,13 @@ class Actions:
24
24
  self.connection = cnt_i
25
25
 
26
26
  def __getattr__(self, item) -> callable:
27
- def wrapper(no_return: bool, **kwargs) -> Manager.Ret | None:
28
- if no_return:
29
- Manager.Packet(
30
- str(item),
31
- **kwargs
32
- ).send_to(self.connection)
33
- return None
34
- else:
35
- packet = Manager.Packet(
36
- str(item),
37
- **kwargs
38
- )
39
- packet.send_to(self.connection)
40
- return get_ret(packet.echo)
27
+ async def wrapper(**kwargs) -> str:
28
+ packet = Manager.Packet(
29
+ str(item),
30
+ **kwargs
31
+ )
32
+ packet.send_to(self.connection)
33
+ return packet.echo
41
34
 
42
35
  return wrapper
43
36
 
@@ -60,7 +53,7 @@ class Actions:
60
53
  else:
61
54
  raise Errors.ArgsInvalidError("'send' API requires 'group_id' or 'user_id' but none of them are provided.")
62
55
  packet.send_to(self.connection)
63
- return get_ret(packet.echo)
56
+ return Manager.Ret.fetch(packet.echo)
64
57
 
65
58
  @Logger.AutoLogAsync.register(Logger.AutoLog.templates().recall, logger)
66
59
  async def del_message(self, message_id: int) -> None:
@@ -90,13 +83,13 @@ class Actions:
90
83
  async def get_login_info(self) -> Manager.Ret:
91
84
  packet = Manager.Packet("get_login_info")
92
85
  packet.send_to(self.connection)
93
- return get_ret(packet.echo)
86
+ return Manager.Ret.fetch(packet.echo)
94
87
 
95
88
  @Logic.Cacher().cache_async
96
89
  async def get_version_info(self) -> Manager.Ret:
97
90
  packet = Manager.Packet("get_version_info")
98
91
  packet.send_to(self.connection)
99
- return get_ret(packet.echo)
92
+ return Manager.Ret.fetch(packet.echo)
100
93
 
101
94
  async def send_forward_msg(self, message: Manager.Message) -> Manager.Ret:
102
95
  packet = Manager.Packet(
@@ -104,7 +97,7 @@ class Actions:
104
97
  messages=await message.get()
105
98
  )
106
99
  packet.send_to(self.connection)
107
- return get_ret(packet.echo)
100
+ return Manager.Ret.fetch(packet.echo)
108
101
 
109
102
  async def send_group_forward_msg(self, group_id: int, message: Manager.Message) -> Manager.Ret:
110
103
  packet = Manager.Packet(
@@ -113,7 +106,7 @@ class Actions:
113
106
  messages=await message.get()
114
107
  )
115
108
  packet.send_to(self.connection)
116
- return get_ret(packet.echo)
109
+ return Manager.Ret.fetch(packet.echo)
117
110
 
118
111
  @Logger.AutoLogAsync.register(Logger.AutoLog.templates().set_req, logger)
119
112
  async def set_group_add_request(self, flag: str, sub_type: str, approve: bool, reason: str = "Refused") -> None:
@@ -133,7 +126,7 @@ class Actions:
133
126
  no_cache=True,
134
127
  )
135
128
  packet.send_to(self.connection)
136
- return get_ret(packet.echo)
129
+ return Manager.Ret.fetch(packet.echo)
137
130
 
138
131
  @Logic.Cacher().cache_async
139
132
  async def get_group_member_info(self, group_id: int, user_id: int) -> Manager.Ret:
@@ -144,7 +137,7 @@ class Actions:
144
137
  no_cache=True
145
138
  )
146
139
  packet.send_to(self.connection)
147
- return get_ret(packet.echo)
140
+ return Manager.Ret.fetch(packet.echo)
148
141
 
149
142
  @Logic.Cacher().cache_async
150
143
  async def get_group_info(self, group_id: int) -> Manager.Ret:
@@ -154,12 +147,12 @@ class Actions:
154
147
  no_cache=True
155
148
  )
156
149
  packet.send_to(self.connection)
157
- return get_ret(packet.echo)
150
+ return Manager.Ret.fetch(packet.echo)
158
151
 
159
152
  async def get_status(self) -> Manager.Ret:
160
153
  packet = Manager.Packet("get_status")
161
154
  packet.send_to(self.connection)
162
- return get_ret(packet.echo)
155
+ return Manager.Ret.fetch(packet.echo)
163
156
 
164
157
  @Logger.AutoLogAsync.register(Logger.AutoLog.templates().set_ess, logger)
165
158
  async def set_essence_msg(self, message_id: int) -> None:
@@ -182,33 +175,37 @@ class Actions:
182
175
  message_id=msg_id
183
176
  )
184
177
  packet.send_to(self.connection)
185
- return get_ret(packet.echo)
178
+ return Manager.Ret.fetch(packet.echo)
186
179
 
187
180
 
188
- async def tester(message_data: Event, actions: Actions) -> None:
181
+ async def tester(message_data: Union[Event, HyperNotify], actions: Actions) -> None:
189
182
  ...
190
183
 
191
184
 
192
- async def __handler(data: dict, actions: Actions) -> None:
193
- if data.get("echo") is not None:
194
- reports.put(Manager.Ret(data))
195
- elif data.get("post_type") == "meta_event" or data.get("user_id") == data.get("self_id"):
196
- pass
185
+ def __handler(data: Union[dict, HyperNotify], actions: Actions) -> None:
186
+ if isinstance(data, dict):
187
+ if data.get("echo") is not None:
188
+ reports.put(data.get("echo"), data)
189
+ elif data.get("post_type") == "meta_event" or data.get("user_id") == data.get("self_id"):
190
+ pass
191
+ else:
192
+ # task = asyncio.create_task(handler(Events.em.new(data), actions))
193
+ asyncio.run(handler(Events.em.new(data), actions))
194
+ # await handler(Events.em.new(data), actions)
195
+ # timed = 0
196
+ #
197
+ # while not task.done():
198
+ # time.sleep(0.1)
199
+ # timed += 0.1
200
+ # if timed >= 30:
201
+ # task.cancel()
202
+ # logger.log(f"处理{task.get_name()}超时", level=Logger.levels.ERROR)
203
+ # break
197
204
  else:
198
- task = asyncio.create_task(handler(Events.em.new(data), actions))
199
- timed = 0
200
-
201
- while not task.done():
202
- await asyncio.sleep(0.1)
203
- timed += 0.1
204
- if timed >= 30:
205
- task.cancel()
206
- logger.log(f"处理{task.get_name()}超时", level=Logger.levels.ERROR)
207
- break
205
+ asyncio.run(handler(data, actions))
208
206
 
209
207
 
210
208
  handler: callable = tester
211
- connection: callable = tester
212
209
 
213
210
 
214
211
  def reg(func: callable):
@@ -216,16 +213,7 @@ def reg(func: callable):
216
213
  handler = func
217
214
 
218
215
 
219
- def get_ret(echo: str) -> Manager.Ret:
220
- old = None
221
- while True:
222
- content: Manager.Ret = reports.get()
223
- if old is not None:
224
- reports.put(old)
225
- if content.echo == echo:
226
- return content
227
- else:
228
- old = content
216
+ connection: Union[Network.WebsocketConnection, Network.HTTPConnection]
229
217
 
230
218
 
231
219
  def run():
@@ -258,6 +246,12 @@ def run():
258
246
  logger.log("成功建立连接", level=Logger.levels.INFO)
259
247
  retried = 0
260
248
  actions = Actions(connection)
249
+ data = HyperListenerStartNotify(
250
+ time_now=int(time.time()),
251
+ notify_type="listener_start",
252
+ connection=connection
253
+ )
254
+ threading.Thread(target=lambda: __handler(data, actions)).start()
261
255
  while True:
262
256
  try:
263
257
  data = connection.recv()
@@ -266,7 +260,10 @@ def run():
266
260
  break
267
261
  except json.decoder.JSONDecodeError:
268
262
  logger.log("收到错误的JSON内容", level=Logger.levels.ERROR)
269
- threading.Thread(target=lambda: asyncio.run(__handler(data, actions))).start()
263
+ continue
264
+ # threading.Thread(target=lambda: asyncio.run(__handler(data, actions))).start()
265
+ threading.Thread(target=lambda: __handler(data, actions)).start()
266
+ # asyncio.create_task(__handler(data, actions))
270
267
  except KeyboardInterrupt:
271
268
  logger.log("正在退出(Ctrl+C)", level=Logger.levels.WARNING)
272
269
  try:
@@ -274,3 +271,11 @@ def run():
274
271
  except:
275
272
  pass
276
273
  os._exit(0)
274
+
275
+
276
+ def stop() -> None:
277
+ try:
278
+ connection.close()
279
+ except:
280
+ pass
281
+ logger.log("停止运行监听器", level=Logger.levels.WARNING)
@@ -1,11 +1,14 @@
1
- from Hyper import Logic, Configurator, Logger, Network
2
- from Hyper.Logger import levels
3
- from Hyper.Segments import *
1
+ import Hyper.Utils.TypeExt
2
+ from Hyper import Configurator, Logger, Network, Segments
3
+ from Hyper.Utils import Logic
4
4
 
5
5
  from typing import Union
6
- import inspect
6
+ # import queue
7
7
  import random
8
+ import json
8
9
 
10
+ # reports = queue.Queue()
11
+ reports = Logic.KeyQueue()
9
12
  config = Configurator.cm.get_cfg()
10
13
  logger = Logger.Logger()
11
14
  logger.set_level(config.log_level)
@@ -33,7 +36,30 @@ class Packet:
33
36
  connection.send(self.endpoint, payload, self.echo)
34
37
 
35
38
 
39
+ class MessageBuilder:
40
+ def __init__(self):
41
+ self.sgs = []
42
+
43
+ def __getattr__(self, item):
44
+ if item == "build":
45
+ def build() -> Message:
46
+ return Message(*self.sgs)
47
+
48
+ return build
49
+
50
+ elif item in Segments.message_types.keys():
51
+ def wrapper(*args, **kwargs):
52
+ self.sgs.append(Segments.message_types[item]["type"](*args, **kwargs))
53
+ return self
54
+
55
+ return wrapper
56
+ else:
57
+ return None
58
+
59
+
36
60
  class Message:
61
+ builder = MessageBuilder()
62
+
37
63
  def __init__(self, *args):
38
64
  if len(args) == 1 and isinstance(args[0], list):
39
65
  contents = args[0]
@@ -47,10 +73,7 @@ class Message:
47
73
  self.contents.append(content)
48
74
 
49
75
  async def get(self) -> list:
50
- ret = []
51
- for i in self.contents:
52
- ret.append(i.to_json())
53
- return ret
76
+ return self.get_sync()
54
77
 
55
78
  def get_sync(self) -> list:
56
79
  ret = []
@@ -90,8 +113,21 @@ class Message:
90
113
 
91
114
 
92
115
  class Ret:
93
- def __init__(self, json_data: dict):
116
+ def __init__(self, json_data: dict, serializer):
94
117
  self.status = json_data["status"]
95
118
  self.ret_code = json_data["retcode"]
96
- self.data = json_data.get("data")
119
+ self.data = serializer(json_data.get("data"))
97
120
  self.echo = json_data.get("echo")
121
+
122
+ @classmethod
123
+ def fetch(cls, echo: str, serializer=Hyper.Utils.TypeExt.ObjectedJson) -> "Ret":
124
+ # old = None
125
+ # while True:
126
+ # content = reports.get()
127
+ # if old is not None:
128
+ # reports.put(old)
129
+ # if content["echo"] == echo:
130
+ # return cls(content)
131
+ # else:
132
+ # old = content
133
+ return cls(reports.get(echo), serializer)
@@ -0,0 +1,111 @@
1
+ message_types = {}
2
+
3
+
4
+ def segment_builder(sg_type: str, summary_tmp: str = None):
5
+ # print(inspect.get_annotations(cls))
6
+ def inner_builder(cls):
7
+ var = dict(vars(cls))
8
+ anns: dict = var.get("__annotations__", False) or dict()
9
+
10
+ def init(self, *args, **kwargs):
11
+ arg = {}
12
+ if len(args) > 0:
13
+ for i in args:
14
+ arg[list(anns.keys())[list(args).index(i)]] = i
15
+
16
+ if len(kwargs) > 0:
17
+ for i in kwargs:
18
+ try:
19
+ arg[i] = anns[i](kwargs[i])
20
+ except TypeError:
21
+ arg[i] = kwargs[i]
22
+ new_arg = arg.copy()
23
+
24
+ if len(anns) > len(arg):
25
+ for i in anns.keys():
26
+ if i not in arg.keys():
27
+ if i not in var.keys():
28
+ new_arg[i] = None
29
+ continue
30
+ if not isinstance(var[i], anns[i]):
31
+ new_arg[i] = anns[i](var[i])
32
+ else:
33
+ new_arg[i] = var[i]
34
+
35
+ for i in new_arg:
36
+ setattr(self, i, new_arg[i])
37
+
38
+ cls.__init__ = init
39
+
40
+ def to_json(self) -> dict:
41
+ base = {"type": sg_type, "data": {}}
42
+ for i in anns:
43
+ if getattr(self, i) is None:
44
+ continue
45
+ if not isinstance(getattr(self, i), anns[i]):
46
+ base["data"][i] = anns[i](getattr(self, i))
47
+ else:
48
+ base["data"][i] = getattr(self, i)
49
+ # try:
50
+ # base["data"][i] = anns[i](getattr(self, i))
51
+ # except TypeError:
52
+ # base["data"][i] = getattr(self, i)
53
+ return base
54
+
55
+ cls.to_json = to_json
56
+
57
+ def to_str(self) -> str:
58
+ text = summary_tmp
59
+ if text is None:
60
+ text = "[]"
61
+ if "<" not in text and ">" not in text:
62
+ return text
63
+
64
+ for i in anns:
65
+ if f"<{i}>" in summary_tmp:
66
+ try:
67
+ v = self.__getattribute__(i)
68
+ except AttributeError:
69
+ v = None
70
+ text = text.replace(f"<{i}>", str(v))
71
+
72
+ return text
73
+
74
+ cls.__str__ = to_str if cls().__str__() == "__not_set__" else cls.__str__
75
+
76
+ def eq(self, other) -> bool:
77
+ if type(self) is type(other) and self.to_json() == other.to_json():
78
+ return True
79
+ else:
80
+ return False
81
+
82
+ cls.__eq__ = eq
83
+
84
+ def ne(self, other) -> bool:
85
+ if type(self) is type(other) and self.to_json() == other.to_json():
86
+ return False
87
+ else:
88
+ return True
89
+
90
+ cls.__ne__ = ne
91
+
92
+ message_types[sg_type] = {
93
+ "type": cls,
94
+ "args": list(anns.keys())
95
+ }
96
+
97
+ return cls
98
+
99
+ return inner_builder
100
+
101
+
102
+ class Base:
103
+ def __init__(self, *args, **kwargs): ...
104
+
105
+ def to_json(self) -> dict: ...
106
+
107
+ def __str__(self) -> str: return "__not_set__"
108
+
109
+ def __eq__(self, other) -> bool: ...
110
+
111
+ def __ne__(self, other) -> bool: ...
@@ -0,0 +1,105 @@
1
+ from Hyper.Adapters.OneBot import *
2
+ from Hyper.Utils.Errors import *
3
+
4
+
5
+ class Actions(Actions):
6
+ def __init__(self, cnt: Union[Network.WebsocketConnection, Network.HTTPConnection, Network.SatoriConnection]):
7
+ self.connection = cnt
8
+
9
+ class CustomAction:
10
+ def __init__(self,
11
+ cnt_i: Union[Network.WebsocketConnection, Network.HTTPConnection, Network.SatoriConnection]):
12
+ self.connection = cnt_i
13
+
14
+ def __getattr__(self, item) -> callable:
15
+ def wrapper(**kwargs) -> str:
16
+ packet = Manager.Packet(
17
+ str(item),
18
+ **kwargs
19
+ )
20
+ packet.send_to(self.connection)
21
+ return packet.echo
22
+
23
+ return wrapper
24
+
25
+ self.custom = CustomAction(self.connection)
26
+
27
+
28
+ async def __handler(data: dict, actions: Actions) -> None:
29
+ if data["op"] == 2:
30
+ pass
31
+ else:
32
+ # task = asyncio.create_task(handler(Events.em.new(data), actions))
33
+ # timed = 0
34
+ #
35
+ # while not task.done():
36
+ # await asyncio.sleep(0.1)
37
+ # timed += 0.1
38
+ # if timed >= 30:
39
+ # task.cancel()
40
+ # logger.log(f"处理{task.get_name()}超时", level=Logger.levels.ERROR)
41
+ # break
42
+ print(data)
43
+
44
+
45
+ def reg(func: callable):
46
+ global handler
47
+ handler = func
48
+
49
+
50
+ connection: Union[Network.WebsocketConnection, Network.SatoriConnection]
51
+
52
+
53
+ def run():
54
+ global connection
55
+ try:
56
+ if handler is tester:
57
+ raise Errors.ListenerNotRegisteredError("No handler registered")
58
+ # connection = websocket.WebSocket()
59
+ if isinstance(config.connection, Configurator.WSConnectionC):
60
+ connection = Network.SatoriConnection(
61
+ config.connection.host, config.connection.port, config.connection.token
62
+ )
63
+ else:
64
+ raise ConfigError
65
+ retried = 0
66
+ while True:
67
+ try:
68
+ connection.connect()
69
+ except ConnectionRefusedError or TimeoutError:
70
+ if retried >= config.connection.retries:
71
+ logger.log(f"重试次数达到最大值({config.connection.retries}),退出", level=Logger.levels.CRITICAL)
72
+ break
73
+
74
+ logger.log(f"连接建立失败,3秒后重试({retried}/{config.connection.retries})",
75
+ level=Logger.levels.WARNING)
76
+ retried += 1
77
+ time.sleep(3)
78
+ continue
79
+ logger.log("成功建立连接", level=Logger.levels.INFO)
80
+ retried = 0
81
+ actions = Actions(connection)
82
+ while True:
83
+ try:
84
+ data = connection.recv()
85
+ except ConnectionResetError:
86
+ logger.log("连接断开", level=Logger.levels.ERROR)
87
+ break
88
+ except json.decoder.JSONDecodeError:
89
+ logger.log("收到错误的JSON内容", level=Logger.levels.ERROR)
90
+ threading.Thread(target=lambda: asyncio.run(__handler(data, actions))).start()
91
+ except KeyboardInterrupt:
92
+ logger.log("正在退出(Ctrl+C)", level=Logger.levels.WARNING)
93
+ try:
94
+ connection.close()
95
+ except:
96
+ pass
97
+ os._exit(0)
98
+
99
+
100
+ def stop() -> None:
101
+ try:
102
+ connection.close()
103
+ except:
104
+ pass
105
+ logger.log("停止运行监听器", level=Logger.levels.WARNING)
@@ -1,13 +1,23 @@
1
+ import json
1
2
  import typing
2
3
 
3
- from Hyper import Logic
4
+ from Hyper.Utils import Logic
4
5
 
5
6
 
6
7
  class WSConnectionC:
7
- def __init__(self, host: str, port: int, retries: int = 0):
8
+ def __init__(self, host: str, port: int, retries: int = 0, satori_token: str = None):
8
9
  self.host: str = host
9
10
  self.port: int = port
10
11
  self.retries: int = retries
12
+ self.token: str = satori_token
13
+
14
+ def to_json(self) -> dict:
15
+ return dict(
16
+ host=self.host,
17
+ port=self.port,
18
+ retries=self.retries,
19
+ satori_token=self.token
20
+ )
11
21
 
12
22
 
13
23
  class HTTPConnectionC:
@@ -18,6 +28,15 @@ class HTTPConnectionC:
18
28
  self.listener_port: int = listener_port
19
29
  self.retries: int = retries
20
30
 
31
+ def to_json(self) -> dict:
32
+ return dict(
33
+ host=self.host,
34
+ port=self.port,
35
+ listener_host=self.listener_host,
36
+ listener_port=self.listener_port,
37
+ retries=self.retries
38
+ )
39
+
21
40
 
22
41
  class Config:
23
42
  def __init__(
@@ -55,7 +74,8 @@ class Config:
55
74
  self.connection = WSConnectionC(
56
75
  config_json["Connection"]["host"],
57
76
  config_json["Connection"]["port"],
58
- config_json["Connection"]["retries"]
77
+ config_json["Connection"]["retries"],
78
+ config_json["Connection"].get("satori_token")
59
79
  )
60
80
  elif config_json["Connection"]["mode"] == "HTTP":
61
81
  self.connection = HTTPConnectionC(
@@ -71,6 +91,21 @@ class Config:
71
91
  self.inited = True
72
92
  return self
73
93
 
94
+ def dump(self, file: str = None) -> None:
95
+ if file or self.file:
96
+ file = file or self.file
97
+ cfg = dict(
98
+ owner=self.owner,
99
+ black_list=self.black_list,
100
+ silents=self.silents,
101
+ Connection=self.connection.to_json(),
102
+ log_level=self.log_level,
103
+ protocol=self.protocol,
104
+ Others=self.others
105
+ )
106
+ with open(file, "w", encoding="utf-8") as f:
107
+ f.write(json.dumps(cfg, indent=2))
108
+
74
109
 
75
110
  class ConfigManager:
76
111
  def __init__(self, config: Config):