satori-python-core 0.16.7__tar.gz → 0.17.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,7 +15,7 @@ dependencies = [
15
15
  description = "Satori Protocol SDK for python, specify common part"
16
16
  license = {text = "MIT"}
17
17
  readme = "README.md"
18
- requires-python = ">=3.9"
18
+ requires-python = ">=3.10,<4.0"
19
19
  classifiers = [
20
20
  "Typing :: Typed",
21
21
  "Development Status :: 4 - Beta",
@@ -31,3 +31,4 @@ classifiers = [
31
31
  [project.urls]
32
32
  homepage = "https://github.com/RF-Tar-Railt/satori-python"
33
33
  repository = "https://github.com/RF-Tar-Railt/satori-python"
34
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: satori-python-core
3
- Version: 0.16.7
3
+ Version: 0.17.0
4
4
  Summary: Satori Protocol SDK for python, specify common part
5
5
  Home-page: https://github.com/RF-Tar-Railt/satori-python
6
6
  Author-Email: RF-Tar-Railt <rf_tar_railt@qq.com>
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Classifier: Operating System :: OS Independent
17
17
  Project-URL: Homepage, https://github.com/RF-Tar-Railt/satori-python
18
18
  Project-URL: Repository, https://github.com/RF-Tar-Railt/satori-python
19
- Requires-Python: >=3.9
19
+ Requires-Python: <4.0,>=3.10
20
20
  Requires-Dist: loguru>=0.7.2
21
21
  Requires-Dist: yarl>=1.9.4
22
22
  Requires-Dist: typing-extensions>=4.7.0
@@ -11,7 +11,7 @@ dependencies = [
11
11
  ]
12
12
  description = "Satori Protocol SDK for python, specify common part"
13
13
  readme = "README.md"
14
- requires-python = ">=3.9"
14
+ requires-python = ">=3.10,<4.0"
15
15
  classifiers = [
16
16
  "Typing :: Typed",
17
17
  "Development Status :: 4 - Beta",
@@ -23,7 +23,7 @@ classifiers = [
23
23
  "Programming Language :: Python :: 3.12",
24
24
  "Operating System :: OS Independent",
25
25
  ]
26
- version = "0.16.7"
26
+ version = "0.17.0"
27
27
 
28
28
  [project.license]
29
29
  text = "MIT"
@@ -39,7 +39,7 @@ requires = [
39
39
  ]
40
40
  build-backend = "mina.backend"
41
41
 
42
- [tool.pdm.dev-dependencies]
42
+ [dependency-groups]
43
43
  dev = [
44
44
  "isort>=5.13.2",
45
45
  "black>=24.4.0",
@@ -49,6 +49,7 @@ dev = [
49
49
  "mina-build<0.6,>=0.5.1",
50
50
  "pdm-mina>=0.3.2",
51
51
  "nonechat<0.7.0,>=0.6.0",
52
+ "uvicorn[standard]>=0.35.0",
52
53
  ]
53
54
 
54
55
  [tool.pdm.build]
@@ -74,21 +75,21 @@ source = "file"
74
75
  path = "src/satori/__init__.py"
75
76
 
76
77
  [tool.black]
77
- line-length = 110
78
+ line-length = 120
78
79
  include = "\\.pyi?$"
79
80
  extend-exclude = ""
80
81
 
81
82
  [tool.isort]
82
83
  profile = "black"
83
- line_length = 110
84
+ line_length = 120
84
85
  skip_gitignore = true
85
86
  extra_standard_library = [
86
87
  "typing_extensions",
87
88
  ]
88
89
 
89
90
  [tool.ruff]
90
- line-length = 110
91
- target-version = "py39"
91
+ line-length = 120
92
+ target-version = "py310"
92
93
  exclude = [
93
94
  "exam.py",
94
95
  ]
@@ -108,12 +109,12 @@ ignore = [
108
109
  "F403",
109
110
  "F405",
110
111
  "C901",
111
- "UP037",
112
+ "UP038",
112
113
  ]
113
114
 
114
115
  [tool.pyright]
115
116
  pythonPlatform = "All"
116
- pythonVersion = "3.9"
117
+ pythonVersion = "3.10"
117
118
  typeCheckingMode = "basic"
118
119
  reportShadowedImports = false
119
120
  disableBytesTypePromotions = true
@@ -23,6 +23,7 @@ from .element import Superscript as Superscript
23
23
  from .element import Text as Text
24
24
  from .element import Underline as Underline
25
25
  from .element import Video as Video
26
+ from .element import register_element as register_element
26
27
  from .element import select as select
27
28
  from .element import transform as transform
28
29
  from .model import ArgvInteraction as ArgvInteraction
@@ -41,4 +42,4 @@ from .model import Role as Role
41
42
  from .model import Upload as Upload
42
43
  from .model import User as User
43
44
 
44
- __version__ = "0.16.7"
45
+ __version__ = "0.17.0"
@@ -1,23 +1,57 @@
1
1
  from base64 import b64encode
2
- from dataclasses import InitVar, dataclass, field, fields
2
+ from collections.abc import Callable, Sequence
3
+ from dataclasses import InitVar, dataclass, field
3
4
  from io import BytesIO
4
5
  from pathlib import Path
5
- from typing import Any, ClassVar, Optional, TypeVar, Union, get_args, overload
6
+ from types import UnionType
7
+ from typing import Any, ClassVar, Final, TypeVar, Union, final, get_args, get_origin, overload
6
8
  from typing_extensions import override
7
9
 
8
10
  from .parser import Element as RawElement
9
- from .parser import escape, param_case
11
+ from .parser import escape, param_case, parse
10
12
  from .parser import select as select_raw
13
+ from .utils import decode
11
14
 
12
15
  TE = TypeVar("TE", bound="Element")
13
16
 
14
17
 
18
+ def conv_bool(v: str) -> bool:
19
+ if v.lower() not in ("true", "false"):
20
+ raise ValueError(v)
21
+ return v.lower() == "true"
22
+
23
+
15
24
  @dataclass(repr=False)
16
25
  class Element:
17
26
  _attrs: dict[str, Any] = field(init=False, default_factory=dict)
18
27
  _children: list["Element"] = field(init=False, default_factory=list)
19
28
 
20
29
  __names__: ClassVar[tuple[str, ...]]
30
+ __convert_fields__: ClassVar[dict[str, Callable[[str], Any]]]
31
+
32
+ def __init_subclass__(cls, **kwargs):
33
+ cls.__convert_fields__ = {}
34
+ annotations = cls.__annotations__
35
+ for name, typ in annotations.items():
36
+ if name.startswith("_"):
37
+ continue
38
+ # _type = get_args(typ)[0] if hasattr(typ, "__origin__") else typ
39
+ orig = get_origin(typ)
40
+ if orig in (Union, UnionType):
41
+ args = get_args(typ)
42
+ if len(args) == 2 and type(None) in args:
43
+ _type = args[0] if args[1] is type(None) else args[1]
44
+ else:
45
+ _type = args[0]
46
+ else:
47
+ _type = typ
48
+ if _type is not str:
49
+ if _type is bool:
50
+ cls.__convert_fields__[name] = conv_bool
51
+ elif _type in (list, dict):
52
+ cls.__convert_fields__[name] = decode
53
+ else:
54
+ cls.__convert_fields__[name] = _type
21
55
 
22
56
  @property
23
57
  def children(self) -> list["Element"]:
@@ -29,24 +63,21 @@ class Element:
29
63
 
30
64
  @classmethod
31
65
  def unpack(cls, attrs: dict[str, Any]):
32
- obj = cls(**{k: v for k, v in attrs.items() if k in cls.__names__}) # type: ignore
33
- obj._attrs.update({k: v for k, v in attrs.items() if k not in cls.__names__})
66
+ data = {}
67
+ names = getattr(cls, "__names__", None)
68
+ for name in cls.__dataclass_fields__.keys():
69
+ if name not in attrs:
70
+ continue
71
+ if name in cls.__convert_fields__:
72
+ data[name] = cls.__convert_fields__[name](attrs[name])
73
+ else:
74
+ data[name] = attrs[name]
75
+ obj = cls(**{k: v for k, v in data.items() if names is None or k in names}) # type: ignore
76
+ obj._attrs.update(data)
34
77
  return obj
35
78
 
36
79
  def __post_init__(self):
37
- for f in fields(self):
38
- if f.name in ("_attrs", "_children"):
39
- continue
40
- _type = get_args(f.type)[0] if hasattr(f.type, "__origin__") else f.type
41
- if _type is not str and isinstance(attr := getattr(self, f.name), str):
42
- if _type is bool:
43
- if attr.lower() not in ("true", "false"):
44
- raise TypeError(f.name, attr)
45
- setattr(self, f.name, attr.lower() == "true")
46
- else:
47
- setattr(self, f.name, _type(attr)) # type: ignore
48
- self._attrs[f.name] = getattr(self, f.name)
49
- self._attrs = {k: v for k, v in self._attrs.items() if v is not None}
80
+ self._attrs = {k: v for k, v in self.__dict__.items() if not k.startswith("_")}
50
81
 
51
82
  def attributes(self) -> str:
52
83
  def _attr(key: str, value: Any):
@@ -82,7 +113,7 @@ class Element:
82
113
  elem += ", { " + ", ".join(repr(i) for i in self._children) + " }"
83
114
  return elem + ")"
84
115
 
85
- def __call__(self, *content: Union[str, "Element"]):
116
+ def __call__(self, *content: "str | Element"):
86
117
  self._children.extend(Text(i) if isinstance(i, str) else i for i in content)
87
118
  self.__post_call__()
88
119
  return self
@@ -103,22 +134,20 @@ class Text(Element):
103
134
  def dumps(self, strip: bool = False) -> str:
104
135
  return self.text if strip else escape(self.text)
105
136
 
106
- __names__ = ("text",)
107
-
108
137
 
109
138
  @dataclass(repr=False)
110
139
  class At(Element):
111
140
  """<at> 元素用于提及某个或某些用户。"""
112
141
 
113
- id: Optional[str] = None
114
- name: Optional[str] = None
115
- role: Optional[str] = None
116
- type: Optional[str] = None
142
+ id: str | None = None
143
+ name: str | None = None
144
+ role: str | None = None
145
+ type: str | None = None
117
146
 
118
147
  @staticmethod
119
148
  def role_(
120
149
  role: str,
121
- name: Optional[str] = None,
150
+ name: str | None = None,
122
151
  ) -> "At":
123
152
  return At(role=role, name=name)
124
153
 
@@ -126,15 +155,13 @@ class At(Element):
126
155
  def all(here: bool = False) -> "At":
127
156
  return At(type="here" if here else "all")
128
157
 
129
- __names__ = ("id", "name", "role", "type")
130
-
131
158
 
132
159
  @dataclass(repr=False)
133
160
  class Sharp(Element):
134
161
  """<sharp> 元素用于提及某个频道。"""
135
162
 
136
163
  id: str
137
- name: Optional[str] = None
164
+ name: str | None = None
138
165
 
139
166
 
140
167
  @dataclass(repr=False)
@@ -143,8 +170,6 @@ class Link(Element):
143
170
 
144
171
  href: str
145
172
 
146
- __names__ = ("href",)
147
-
148
173
  def __post_call__(self):
149
174
  if not self._children:
150
175
  return
@@ -165,26 +190,26 @@ class Link(Element):
165
190
  @dataclass(repr=False)
166
191
  class Resource(Element):
167
192
  src: str
168
- title: Optional[str] = None
169
- extra: InitVar[Optional[dict[str, Any]]] = None
170
- cache: Optional[bool] = None
171
- timeout: Optional[int] = None
193
+ title: str | None = None
194
+ extra: InitVar[dict[str, Any] | None] = None
195
+ cache: bool | None = None
196
+ timeout: int | None = None
172
197
 
173
198
  __names__ = ("src", "title")
174
199
 
175
200
  @classmethod
176
201
  def of(
177
202
  cls,
178
- url: Optional[str] = None,
179
- path: Optional[Union[str, Path]] = None,
180
- raw: Optional[Union[bytes, BytesIO]] = None,
181
- mime: Optional[str] = None,
182
- name: Optional[str] = None,
183
- duration: Optional[float] = None,
184
- poster: Optional[str] = None,
185
- extra: Optional[dict[str, Any]] = None,
186
- cache: Optional[bool] = None,
187
- timeout: Optional[int] = None,
203
+ url: str | None = None,
204
+ path: str | Path | None = None,
205
+ raw: bytes | BytesIO | None = None,
206
+ mime: str | None = None,
207
+ name: str | None = None,
208
+ duration: float | None = None,
209
+ poster: str | None = None,
210
+ extra: dict[str, Any] | None = None,
211
+ cache: bool | None = None,
212
+ timeout: int | None = None,
188
213
  **kwargs,
189
214
  ):
190
215
  data: dict[str, Any] = {"extra": extra or kwargs}
@@ -209,7 +234,7 @@ class Resource(Element):
209
234
  data["timeout"] = timeout
210
235
  return cls(**data)
211
236
 
212
- def __post_init__(self, extra: Optional[dict[str, Any]] = None):
237
+ def __post_init__(self, extra: dict[str, Any] | None = None):
213
238
  super().__post_init__()
214
239
  if extra:
215
240
  self._attrs.update(extra)
@@ -219,8 +244,8 @@ class Resource(Element):
219
244
  class Image(Resource):
220
245
  """<img> 元素用于表示图片。"""
221
246
 
222
- width: Optional[int] = None
223
- height: Optional[int] = None
247
+ width: int | None = None
248
+ height: int | None = None
224
249
 
225
250
  __names__ = ("src", "title", "width", "height")
226
251
 
@@ -234,8 +259,8 @@ class Image(Resource):
234
259
  class Audio(Resource):
235
260
  """<audio> 元素用于表示语音。"""
236
261
 
237
- duration: Optional[float] = None
238
- poster: Optional[str] = None
262
+ duration: float | None = None
263
+ poster: str | None = None
239
264
 
240
265
  __names__ = ("src", "title", "duration", "poster")
241
266
 
@@ -244,10 +269,10 @@ class Audio(Resource):
244
269
  class Video(Resource):
245
270
  """<video> 元素用于表示视频。"""
246
271
 
247
- width: Optional[int] = None
248
- height: Optional[int] = None
249
- duration: Optional[float] = None
250
- poster: Optional[str] = None
272
+ width: int | None = None
273
+ height: int | None = None
274
+ duration: float | None = None
275
+ poster: str | None = None
251
276
 
252
277
  __names__ = ("src", "title", "width", "height", "duration", "poster")
253
278
 
@@ -256,7 +281,7 @@ class Video(Resource):
256
281
  class File(Resource):
257
282
  """<file> 元素用于表示文件。"""
258
283
 
259
- poster: Optional[str] = None
284
+ poster: str | None = None
260
285
 
261
286
  __names__ = ("src", "title", "poster")
262
287
 
@@ -267,7 +292,7 @@ class Style(Element):
267
292
 
268
293
  __names__ = ()
269
294
 
270
- def __init__(self, *text: Union[str, Text, "Style"]):
295
+ def __init__(self, *text: "str | Text | Style"):
271
296
  super().__init__()
272
297
  self.__call__(*text)
273
298
 
@@ -374,16 +399,14 @@ class Message(Element):
374
399
  子元素对应于消息的内容。如果其没有子元素,则消息不会被发送。
375
400
  """
376
401
 
377
- __names__ = ("id", "forward")
378
-
379
- id: Optional[str]
380
- forward: Optional[bool]
402
+ id: str | None
403
+ forward: bool | None
381
404
 
382
405
  def __init__(
383
406
  self,
384
- id: Optional[str] = None,
385
- forward: Optional[bool] = None,
386
- content: Optional[list[Union[str, Element]]] = None,
407
+ id: str | None = None,
408
+ forward: bool | None = None,
409
+ content: list[str | Element] | None = None,
387
410
  ):
388
411
  self.id = id
389
412
  self.forward = forward
@@ -405,10 +428,8 @@ class Author(Element):
405
428
  """<author> 元素用于表示消息的作者。它的子元素会被渲染为作者的名字。"""
406
429
 
407
430
  id: str
408
- name: Optional[str] = None
409
- avatar: Optional[str] = None
410
-
411
- __names__ = ("id", "name", "avatar")
431
+ name: str | None = None
432
+ avatar: str | None = None
412
433
 
413
434
 
414
435
  @dataclass(repr=False)
@@ -416,23 +437,21 @@ class Button(Element):
416
437
  """<button> 元素用于表示一个按钮。它的子元素会被渲染为按钮的文本。"""
417
438
 
418
439
  type: str
419
- id: Optional[str] = None
420
- href: Optional[str] = None
421
- text: Optional[str] = None
422
- theme: Optional[str] = None
423
-
424
- __names__ = ("type", "id", "href", "text", "theme")
440
+ id: str | None = None
441
+ href: str | None = None
442
+ text: str | None = None
443
+ theme: str | None = None
425
444
 
426
445
  @classmethod
427
- def action(cls, button_id: str, theme: Optional[str] = None):
446
+ def action(cls, button_id: str, theme: str | None = None):
428
447
  return Button("action", id=button_id, theme=theme)
429
448
 
430
449
  @classmethod
431
- def link(cls, url: str, theme: Optional[str] = None):
450
+ def link(cls, url: str, theme: str | None = None):
432
451
  return Button("link", href=url, theme=theme)
433
452
 
434
453
  @classmethod
435
- def input(cls, text: str, theme: Optional[str] = None):
454
+ def input(cls, text: str, theme: str | None = None):
436
455
  return Button("input", text=text, theme=theme)
437
456
 
438
457
  def attributes(self) -> str:
@@ -460,18 +479,15 @@ class Button(Element):
460
479
  class Custom(Element):
461
480
  """自定义元素用于构造标准元素以外的元素"""
462
481
 
463
- type: str
464
-
465
482
  __names__ = ()
466
483
 
467
484
  def __init__(
468
485
  self,
469
486
  type: str,
470
- attrs: Optional[dict[str, Any]] = None,
471
- children: Optional[list[Union[str, Element]]] = None,
487
+ attrs: dict[str, Any] | None = None,
488
+ children: Sequence[str | Element] | None = None,
472
489
  ):
473
490
  self.type = type
474
- super().__init__()
475
491
  if not hasattr(self, "_attrs"):
476
492
  self._attrs = attrs or {}
477
493
  else:
@@ -500,6 +516,17 @@ class Raw(Element):
500
516
  return self.content if strip else escape(self.content)
501
517
 
502
518
 
519
+ def register_element(cls: type[TE], tag: str | None = None) -> type[TE]:
520
+ """注册一个自定义元素类,使其可以被 `transform` 函数识别。
521
+
522
+ 该类必须继承自 `Element`; 必要时还需要定义 `__names__` 类变量以指定可接受的属性名。
523
+ """
524
+ if not issubclass(cls, Element):
525
+ raise TypeError("cls must be a subclass of Element")
526
+ ELEMENT_TYPE_MAP[tag or cls.__name__.lower()] = cls
527
+ return cls
528
+
529
+
503
530
  ELEMENT_TYPE_MAP = {
504
531
  "text": Text,
505
532
  "at": At,
@@ -571,14 +598,14 @@ def transform(elements: list[RawElement]) -> list[Element]:
571
598
 
572
599
 
573
600
  @overload
574
- def select(elements: Union[Element, list[Element]], query: type[TE]) -> list[TE]: ...
601
+ def select(elements: Element | list[Element], query: type[TE]) -> list[TE]: ...
575
602
 
576
603
 
577
604
  @overload
578
- def select(elements: Union[Element, list[Element]], query: str) -> list[Element]: ...
605
+ def select(elements: Element | list[Element], query: str) -> list[Element]: ...
579
606
 
580
607
 
581
- def select(elements: Union[Element, list[Element]], query: Union[type[TE], str]):
608
+ def select(elements: Element | list[Element], query: type[TE] | str):
582
609
  if not elements:
583
610
  return []
584
611
  if isinstance(elements, Element):
@@ -596,39 +623,44 @@ def select(elements: Union[Element, list[Element]], query: Union[type[TE], str])
596
623
  return results
597
624
 
598
625
 
599
- class E:
600
- text = Text
601
- at = At
602
- at_role = At.role_
603
- at_all = At.all
604
- sharp = Sharp
605
- link = Link
606
- image = Image.of
607
- audio = Audio.of
608
- video = Video.of
609
- file = File.of
610
- resource = Resource
611
- bold = Bold
612
- italic = Italic
613
- underline = Underline
614
- strikethrough = Strikethrough
615
- spoiler = Spoiler
616
- code = Code
617
- sup = Superscript
618
- sub = Subscript
619
- br = Br
620
- paragraph = Paragraph
621
- message = Message
622
- quote = Quote
623
- author = Author
624
- custom = Custom
625
- raw = Raw
626
- button = Button
627
- action_button = Button.action
628
- link_button = Button.link
629
- input_button = Button.input
630
-
631
- select = select
632
-
633
- def __new__(cls, *args, **kwargs):
634
- raise TypeError("E is not instantiable")
626
+ @final
627
+ class _E:
628
+ def __init__(self):
629
+ self.text = Text
630
+ self.at = At
631
+ self.at_role = At.role_
632
+ self.at_all = At.all
633
+ self.sharp = Sharp
634
+ self.link = Link
635
+ self.image = Image.of
636
+ self.audio = Audio.of
637
+ self.video = Video.of
638
+ self.file = File.of
639
+ self.resource = Resource
640
+ self.bold = Bold
641
+ self.italic = Italic
642
+ self.underline = Underline
643
+ self.strikethrough = Strikethrough
644
+ self.spoiler = Spoiler
645
+ self.code = Code
646
+ self.sup = Superscript
647
+ self.sub = Subscript
648
+ self.br = Br
649
+ self.paragraph = Paragraph
650
+ self.message = Message
651
+ self.quote = Quote
652
+ self.author = Author
653
+ self.raw = Raw
654
+ self.button = Button
655
+ self.action_button = Button.action
656
+ self.link_button = Button.link
657
+ self.input_button = Button.input
658
+ self.select = select
659
+
660
+ def __call__(self, elem: str, context: dict | None = None) -> Custom:
661
+ """创建一个自定义元素"""
662
+ e = parse(elem, context)[0]
663
+ return Custom(e.type, e.attrs, transform(e.children))
664
+
665
+
666
+ E: Final = _E()
@@ -1,12 +1,12 @@
1
1
  import mimetypes
2
- from collections.abc import AsyncIterable, Awaitable
3
- from dataclasses import asdict, dataclass, field, fields
2
+ from collections.abc import AsyncIterable, Awaitable, Callable
3
+ from dataclasses import Field, dataclass, field
4
4
  from datetime import datetime
5
5
  from enum import IntEnum
6
6
  from os import PathLike
7
7
  from pathlib import Path
8
- from typing import IO, Any, Callable, ClassVar, Generic, Literal, Optional, TypeVar, Union
9
- from typing_extensions import TypeAlias
8
+ from typing import IO, Any, ClassVar, Generic, Literal, TypeAlias, TypeVar
9
+ from typing_extensions import Self
10
10
 
11
11
  from .element import Element, transform
12
12
  from .parser import Element as RawElement
@@ -19,15 +19,15 @@ class ModelBase:
19
19
  _raw_data: dict[str, Any] = field(init=False, default_factory=dict, repr=False, compare=False, hash=False)
20
20
 
21
21
  @classmethod
22
- def parse(cls, raw: dict):
23
- fs = fields(cls)
22
+ def parse(cls: type[Self], raw: dict) -> Self:
23
+ fs: dict[str, Field] = cls.__dataclass_fields__
24
24
  data = {}
25
- for fd in fs:
26
- if fd.name in raw:
27
- if fd.name in cls.__converter__:
28
- data[fd.name] = cls.__converter__[fd.name](raw[fd.name])
25
+ for name in fs.keys():
26
+ if name in raw:
27
+ if name in cls.__converter__:
28
+ data[name] = cls.__converter__[name](raw[name])
29
29
  else:
30
- data[fd.name] = raw[fd.name]
30
+ data[name] = raw[name]
31
31
  obj = cls(**data) # type: ignore
32
32
  obj._raw_data = raw
33
33
  return obj
@@ -47,8 +47,8 @@ class ChannelType(IntEnum):
47
47
  class Channel(ModelBase):
48
48
  id: str
49
49
  type: ChannelType = ChannelType.TEXT
50
- name: Optional[str] = None
51
- parent_id: Optional[str] = None
50
+ name: str | None = None
51
+ parent_id: str | None = None
52
52
 
53
53
  __converter__ = {"type": ChannelType}
54
54
 
@@ -64,8 +64,8 @@ class Channel(ModelBase):
64
64
  @dataclass
65
65
  class Guild(ModelBase):
66
66
  id: str
67
- name: Optional[str] = None
68
- avatar: Optional[str] = None
67
+ name: str | None = None
68
+ avatar: str | None = None
69
69
 
70
70
  def dump(self):
71
71
  res = {"id": self.id}
@@ -79,10 +79,10 @@ class Guild(ModelBase):
79
79
  @dataclass
80
80
  class User(ModelBase):
81
81
  id: str
82
- name: Optional[str] = None
83
- nick: Optional[str] = None
84
- avatar: Optional[str] = None
85
- is_bot: Optional[bool] = None
82
+ name: str | None = None
83
+ nick: str | None = None
84
+ avatar: str | None = None
85
+ is_bot: bool | None = None
86
86
 
87
87
  def dump(self):
88
88
  res: dict[str, Any] = {"id": self.id}
@@ -99,10 +99,10 @@ class User(ModelBase):
99
99
 
100
100
  @dataclass
101
101
  class Member(ModelBase):
102
- user: Optional[User] = None
103
- nick: Optional[str] = None
104
- avatar: Optional[str] = None
105
- joined_at: Optional[datetime] = None
102
+ user: User | None = None
103
+ nick: str | None = None
104
+ avatar: str | None = None
105
+ joined_at: datetime | None = None
106
106
 
107
107
  __converter__ = {"user": User.parse, "joined_at": lambda ts: datetime.fromtimestamp(int(ts) / 1000)}
108
108
 
@@ -122,7 +122,7 @@ class Member(ModelBase):
122
122
  @dataclass
123
123
  class Role(ModelBase):
124
124
  id: str
125
- name: Optional[str] = None
125
+ name: str | None = None
126
126
 
127
127
  def dump(self):
128
128
  res = {"id": self.id}
@@ -188,8 +188,8 @@ class Login(ModelBase):
188
188
 
189
189
  @dataclass
190
190
  class LoginPartial(Login):
191
- platform: Optional[str] = None
192
- user: Optional[User] = None
191
+ platform: str | None = None
192
+ user: User | None = None
193
193
 
194
194
 
195
195
  @dataclass
@@ -199,7 +199,7 @@ class ArgvInteraction(ModelBase):
199
199
  options: Any
200
200
 
201
201
  def dump(self):
202
- return asdict(self)
202
+ return {"name": self.name, "arguments": self.arguments, "options": self.options}
203
203
 
204
204
 
205
205
  @dataclass
@@ -207,7 +207,7 @@ class ButtonInteraction(ModelBase):
207
207
  id: str
208
208
 
209
209
  def dump(self):
210
- return asdict(self)
210
+ return {"id": self.id}
211
211
 
212
212
 
213
213
  class Opcode(IntEnum):
@@ -227,8 +227,8 @@ class Opcode(IntEnum):
227
227
 
228
228
  @dataclass
229
229
  class Identify(ModelBase):
230
- token: Optional[str] = None
231
- sn: Optional[int] = None
230
+ token: str | None = None
231
+ sn: int | None = None
232
232
 
233
233
  @classmethod
234
234
  def parse(cls, raw: dict):
@@ -237,11 +237,11 @@ class Identify(ModelBase):
237
237
  return super().parse(raw)
238
238
 
239
239
  @property
240
- def sequence(self) -> Optional[int]:
240
+ def sequence(self) -> int | None:
241
241
  return self.sn
242
242
 
243
243
  def dump(self):
244
- return asdict(self)
244
+ return {k: v for k, v in (("token", self.token), ("sn", self.sn)) if v is not None}
245
245
 
246
246
 
247
247
  @dataclass
@@ -252,7 +252,7 @@ class Ready(ModelBase):
252
252
  __converter__ = {"logins": lambda raw: [LoginPartial.parse(login) for login in raw]}
253
253
 
254
254
  def dump(self):
255
- return asdict(self)
255
+ return {"logins": [login.dump() for login in self.logins], "proxy_urls": self.proxy_urls}
256
256
 
257
257
 
258
258
  @dataclass
@@ -262,7 +262,7 @@ class MetaPayload(ModelBase):
262
262
  proxy_urls: list[str]
263
263
 
264
264
  def dump(self):
265
- return asdict(self)
265
+ return {"proxy_urls": self.proxy_urls}
266
266
 
267
267
 
268
268
  @dataclass
@@ -275,40 +275,46 @@ class Meta(ModelBase):
275
275
  __converter__ = {"logins": lambda raw: [LoginPartial.parse(login) for login in raw]}
276
276
 
277
277
  def dump(self):
278
- return asdict(self)
278
+ return {"logins": [login.dump() for login in self.logins], "proxy_urls": self.proxy_urls}
279
279
 
280
280
 
281
281
  @dataclass
282
282
  class MessageObject(ModelBase):
283
283
  id: str
284
284
  content: str
285
- channel: Optional[Channel] = None
286
- guild: Optional[Guild] = None
287
- member: Optional[Member] = None
288
- user: Optional[User] = None
289
- created_at: Optional[datetime] = None
290
- updated_at: Optional[datetime] = None
285
+ channel: Channel | None = None
286
+ guild: Guild | None = None
287
+ member: Member | None = None
288
+ user: User | None = None
289
+ created_at: datetime | None = None
290
+ updated_at: datetime | None = None
291
291
 
292
292
  @classmethod
293
293
  def from_elements(
294
294
  cls,
295
295
  id: str,
296
296
  content: list[Element],
297
- channel: Optional[Channel] = None,
298
- guild: Optional[Guild] = None,
299
- member: Optional[Member] = None,
300
- user: Optional[User] = None,
301
- created_at: Optional[datetime] = None,
302
- updated_at: Optional[datetime] = None,
297
+ channel: Channel | None = None,
298
+ guild: Guild | None = None,
299
+ member: Member | None = None,
300
+ user: User | None = None,
301
+ created_at: datetime | None = None,
302
+ updated_at: datetime | None = None,
303
303
  ):
304
- return cls(id, "".join(str(i) for i in content), channel, guild, member, user, created_at, updated_at)
304
+ obj = cls(id, "".join(str(i) for i in content), channel, guild, member, user, created_at, updated_at)
305
+ obj._parsed_message = content
306
+ return obj
305
307
 
306
308
  @property
307
309
  def message(self) -> list[Element]:
308
- return transform(parse(self.content))
310
+ if hasattr(self, "_parsed_message"):
311
+ return self._parsed_message
312
+ self._parsed_message = transform(parse(self.content))
313
+ return self._parsed_message
309
314
 
310
315
  @message.setter
311
316
  def message(self, value: list[Element]):
317
+ self._parsed_message = value
312
318
  self.content = "".join(str(i) for i in value)
313
319
 
314
320
  @classmethod
@@ -347,22 +353,22 @@ class MessageObject(ModelBase):
347
353
  @dataclass
348
354
  class MessageReceipt(ModelBase):
349
355
  id: str
350
- content: Optional[str] = None
356
+ content: str | None = None
351
357
 
352
358
  @classmethod
353
359
  def from_elements(
354
360
  cls,
355
361
  id: str,
356
- content: Optional[list[Element]] = None,
362
+ content: list[Element] | None = None,
357
363
  ):
358
364
  return cls(id, "".join(str(i) for i in content) if content else None)
359
365
 
360
366
  @property
361
- def message(self) -> Optional[list[Element]]:
367
+ def message(self) -> list[Element] | None:
362
368
  return transform(parse(self.content)) if self.content else None
363
369
 
364
370
  @message.setter
365
- def message(self, value: Optional[list[Element]]):
371
+ def message(self, value: list[Element] | None):
366
372
  self.content = "".join(str(i) for i in value) if value else None
367
373
 
368
374
  @classmethod
@@ -384,18 +390,18 @@ class Event(ModelBase):
384
390
  type: str
385
391
  timestamp: datetime
386
392
  login: Login
387
- argv: Optional[ArgvInteraction] = None
388
- button: Optional[ButtonInteraction] = None
389
- channel: Optional[Channel] = None
390
- guild: Optional[Guild] = None
391
- member: Optional[Member] = None
392
- message: Optional[MessageObject] = None
393
- operator: Optional[User] = None
394
- role: Optional[Role] = None
395
- user: Optional[User] = None
396
-
397
- _type: Optional[str] = None
398
- _data: Optional[dict] = None
393
+ argv: ArgvInteraction | None = None
394
+ button: ButtonInteraction | None = None
395
+ channel: Channel | None = None
396
+ guild: Guild | None = None
397
+ member: Member | None = None
398
+ message: MessageObject | None = None
399
+ operator: User | None = None
400
+ role: Role | None = None
401
+ user: User | None = None
402
+
403
+ _type: str | None = None
404
+ _data: dict | None = None
399
405
 
400
406
  sn: int = 0
401
407
 
@@ -474,10 +480,10 @@ T = TypeVar("T", bound=ModelBase)
474
480
  @dataclass
475
481
  class PageResult(ModelBase, Generic[T]):
476
482
  data: list[T]
477
- next: Optional[str] = None
483
+ next: str | None = None
478
484
 
479
485
  @classmethod
480
- def parse(cls, raw: dict, parser: Optional[Callable[[dict], T]] = None) -> "PageResult[T]":
486
+ def parse(cls, raw: dict, parser: Callable[[dict], T] | None = None) -> "PageResult[T]":
481
487
  data = [(parser or ModelBase.parse)(item) for item in raw["data"]]
482
488
  return cls(data, raw.get("next")) # type: ignore
483
489
 
@@ -490,10 +496,10 @@ class PageResult(ModelBase, Generic[T]):
490
496
 
491
497
  @dataclass
492
498
  class PageDequeResult(PageResult[T]):
493
- prev: Optional[str] = None
499
+ prev: str | None = None
494
500
 
495
501
  @classmethod
496
- def parse(cls, raw: dict, parser: Optional[Callable[[dict], T]] = None) -> "PageDequeResult[T]":
502
+ def parse(cls, raw: dict, parser: Callable[[dict], T] | None = None) -> "PageDequeResult[T]":
497
503
  data = [(parser or ModelBase.parse)(item) for item in raw["data"]]
498
504
  return cls(data, raw.get("next"), raw.get("prev")) # type: ignore
499
505
 
@@ -507,9 +513,7 @@ class PageDequeResult(PageResult[T]):
507
513
 
508
514
 
509
515
  class IterablePageResult(Generic[T], AsyncIterable[T], Awaitable[PageResult[T]]):
510
- def __init__(
511
- self, func: Callable[[Optional[str]], Awaitable[PageResult[T]]], initial_page: Optional[str] = None
512
- ):
516
+ def __init__(self, func: Callable[[str | None], Awaitable[PageResult[T]]], initial_page: str | None = None):
513
517
  self.func = func
514
518
  self.next_page = initial_page
515
519
 
@@ -535,9 +539,9 @@ Order: TypeAlias = Literal["asc", "desc"]
535
539
 
536
540
  @dataclass
537
541
  class Upload:
538
- file: Union[bytes, IO[bytes], PathLike]
542
+ file: bytes | IO[bytes] | PathLike
539
543
  mimetype: str = "image/png"
540
- name: Optional[str] = None
544
+ name: str | None = None
541
545
 
542
546
  def __post_init__(self):
543
547
  if isinstance(self.file, PathLike):
@@ -1,9 +1,8 @@
1
1
  import re
2
- from collections.abc import Iterable
2
+ from collections.abc import Callable, Iterable
3
3
  from dataclasses import dataclass, field
4
4
  from enum import IntEnum
5
- from typing import Any, Callable, Literal, Optional, TypedDict, TypeVar, Union, cast
6
- from typing_extensions import TypeAlias
5
+ from typing import Any, Literal, Optional, TypeAlias, TypedDict, TypeVar, Union, cast
7
6
 
8
7
  T = TypeVar("T")
9
8
 
@@ -29,18 +28,14 @@ def camel_case(source: str) -> str:
29
28
 
30
29
 
31
30
  def param_case(source: str) -> str:
32
- return re.sub(
33
- ".[A-Z]+", lambda mat: mat[0][0] + "-" + mat[0][1:].lower(), uncapitalize(source).replace("_", "-")
34
- )
31
+ return re.sub(".[A-Z]+", lambda mat: mat[0][0] + "-" + mat[0][1:].lower(), uncapitalize(source).replace("_", "-"))
35
32
 
36
33
 
37
34
  def snake_case(source: str) -> str:
38
- return re.sub(
39
- ".[A-Z]", lambda mat: mat[0][0] + "_" + mat[0][1:].lower(), uncapitalize(source).replace("-", "_")
40
- )
35
+ return re.sub(".[A-Z]", lambda mat: mat[0][0] + "_" + mat[0][1:].lower(), uncapitalize(source).replace("-", "_"))
41
36
 
42
37
 
43
- def ensure_list(value: Union[T, list[T], None]) -> list[T]:
38
+ def ensure_list(value: T | list[T] | None) -> list[T]:
44
39
  return value if isinstance(value, list) else [value] if value else []
45
40
 
46
41
 
@@ -73,12 +68,12 @@ class Element:
73
68
  type: str
74
69
  attrs: dict[str, Any]
75
70
  children: list["Element"]
76
- source: Optional[str] = None
71
+ source: str | None = None
77
72
 
78
73
  def __init__(
79
74
  self,
80
- type: Union[str, Render[Fragment, Any]],
81
- attrs: Optional[dict[str, Any]] = None,
75
+ type: str | Render[Fragment, Any],
76
+ attrs: dict[str, Any] | None = None,
82
77
  *children: Fragment,
83
78
  ) -> None:
84
79
  self.attrs = {}
@@ -174,7 +169,7 @@ def parse_selector(input: str) -> list[list[Selector]]:
174
169
  return [_quert(q) for q in input.split(",")]
175
170
 
176
171
 
177
- def select(source: Union[str, list[Element]], query: Union[str, list[list[Selector]]]) -> list[Element]:
172
+ def select(source: str | list[Element], query: str | list[list[Selector]]) -> list[Element]:
178
173
  if not source or not query:
179
174
  return []
180
175
  if isinstance(source, str):
@@ -237,9 +232,7 @@ tag_pat2 = re.compile(
237
232
  r"(?P<comment><!--[\s\S]*?-->)|(?P<tag><(/?)([^!\s>/]*)([^>]*?)\s*(/?)>)|(?P<curly>\{(?P<derivative>[@:/#][^\s\}]*)?[\s\S]*?\})"
238
233
  )
239
234
  attr_pat1 = re.compile(r"([^\s=]+)(?:=\"(?P<value1>[^\"]*)\"|='(?P<value2>[^']*)')?", re.S)
240
- attr_pat2 = re.compile(
241
- r"([^\s=]+)(?:=\"(?P<value1>[^\"]*)\"|='(?P<value2>[^']*)'|=\{(?P<value3>[^\}]+)\})?", re.S
242
- )
235
+ attr_pat2 = re.compile(r"([^\s=]+)(?:=\"(?P<value1>[^\"]*)\"|='(?P<value2>[^']*)'|=\{(?P<curly>[^\}]+)\})?", re.S)
243
236
 
244
237
 
245
238
  class Position(IntEnum):
@@ -264,7 +257,7 @@ class StackItem(TypedDict):
264
257
  slot: str
265
258
 
266
259
 
267
- def fold_tokens(tokens: list[Union[str, Token]]) -> list[Union[str, Token]]:
260
+ def fold_tokens(tokens: list[str | Token]) -> list[str | Token]:
268
261
  stack: list[StackItem] = [
269
262
  {
270
263
  "token": Token(
@@ -279,7 +272,7 @@ def fold_tokens(tokens: list[Union[str, Token]]) -> list[Union[str, Token]]:
279
272
  }
280
273
  ]
281
274
 
282
- def push_token(*tokens: Union[str, Token]):
275
+ def push_token(*tokens: str | Token):
283
276
  token = stack[0]["token"]
284
277
  token.children[stack[0]["slot"]].extend(tokens)
285
278
 
@@ -302,7 +295,7 @@ def fold_tokens(tokens: list[Union[str, Token]]) -> list[Union[str, Token]]:
302
295
  return stack[-1]["token"].children["default"]
303
296
 
304
297
 
305
- def parse_tokens(tokens: list[Union[str, Token]], context: Optional[dict] = None) -> list[Element]:
298
+ def parse_tokens(tokens: list[str | Token], context: dict | None = None) -> list[Element]:
306
299
  result: list[Element] = []
307
300
  for token in tokens:
308
301
  if isinstance(token, str):
@@ -313,12 +306,12 @@ def parse_tokens(tokens: list[Union[str, Token]], context: Optional[dict] = None
313
306
  while mat := attr_pat.search(token.extra):
314
307
  key = mat.group(1)
315
308
  groupdict = mat.groupdict()
316
- v = groupdict.get("value1") or groupdict.get("value2")
317
- v3 = groupdict.get("value3")
318
- if v3 and context is not None:
319
- attrs[key] = interpolate(v3, context)
320
- elif v is not None:
321
- attrs[key] = unescape(v)
309
+ v2 = groupdict.get("value2", groupdict.get("value1"))
310
+ curly = groupdict.get("curly")
311
+ if curly and context is not None:
312
+ attrs[key] = interpolate(curly, context)
313
+ elif v2 is not None:
314
+ attrs[key] = unescape(v2)
322
315
  elif key.startswith("no-"):
323
316
  attrs[key[3:]] = False
324
317
  else:
@@ -348,8 +341,8 @@ def parse_tokens(tokens: list[Union[str, Token]], context: Optional[dict] = None
348
341
  return result
349
342
 
350
343
 
351
- def parse(src: str, context: Optional[dict] = None):
352
- tokens: list[Union[str, Token]] = []
344
+ def parse(src: str, context: dict | None = None):
345
+ tokens: list[str | Token] = []
353
346
 
354
347
  def push_text(text: str):
355
348
  if text:
@@ -0,0 +1,33 @@
1
+ import socket
2
+
3
+
4
+ def get_public_ip():
5
+ st = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
6
+ try:
7
+ st.connect(("10.255.255.255", 1))
8
+ IP = st.getsockname()[0]
9
+ except Exception:
10
+ IP = "localhost"
11
+ finally:
12
+ st.close()
13
+ return IP
14
+
15
+
16
+ try:
17
+ from msgspec.json import Decoder, Encoder # noqa: F401
18
+
19
+ decoder = Decoder()
20
+ encoder = Encoder()
21
+
22
+ decode = decoder.decode
23
+
24
+ def encode(obj):
25
+ return encoder.encode(obj).decode()
26
+
27
+ except ImportError:
28
+ import json
29
+
30
+ def encode(obj):
31
+ return json.dumps(obj, separators=(",", ":"), ensure_ascii=False)
32
+
33
+ decode = json.loads
@@ -1,17 +0,0 @@
1
- import socket
2
-
3
-
4
- def get_public_ip():
5
- st = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
6
- try:
7
- st.connect(("10.255.255.255", 1))
8
- IP = st.getsockname()[0]
9
- except Exception:
10
- IP = "localhost"
11
- finally:
12
- st.close()
13
- return IP
14
-
15
-
16
- if __name__ == "__main__":
17
- print(get_public_ip()) # noqa: T201