omlish 0.0.0.dev222__py3-none-any.whl → 0.0.0.dev224__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.
Files changed (35) hide show
  1. omlish/__about__.py +4 -4
  2. omlish/asyncs/asyncio/subprocesses.py +15 -23
  3. omlish/dynamic.py +1 -1
  4. omlish/funcs/genmachine.py +1 -1
  5. omlish/http/coro/fdio.py +3 -3
  6. omlish/http/coro/server.py +1 -1
  7. omlish/http/handlers.py +65 -0
  8. omlish/http/parsing.py +13 -0
  9. omlish/lite/marshal.py +22 -6
  10. omlish/lite/timing.py +8 -0
  11. omlish/logs/timing.py +58 -0
  12. omlish/secrets/tempssl.py +43 -21
  13. omlish/specs/irc/messages/__init__.py +0 -0
  14. omlish/specs/irc/messages/base.py +49 -0
  15. omlish/specs/irc/messages/formats.py +92 -0
  16. omlish/specs/irc/messages/messages.py +774 -0
  17. omlish/specs/irc/messages/parsing.py +98 -0
  18. omlish/specs/irc/numerics/numerics.py +57 -0
  19. omlish/subprocesses.py +107 -0
  20. {omlish-0.0.0.dev222.dist-info → omlish-0.0.0.dev224.dist-info}/METADATA +5 -5
  21. {omlish-0.0.0.dev222.dist-info → omlish-0.0.0.dev224.dist-info}/RECORD +35 -28
  22. /omlish/specs/irc/{format → protocol}/LICENSE +0 -0
  23. /omlish/specs/irc/{format → protocol}/__init__.py +0 -0
  24. /omlish/specs/irc/{format → protocol}/consts.py +0 -0
  25. /omlish/specs/irc/{format → protocol}/errors.py +0 -0
  26. /omlish/specs/irc/{format → protocol}/message.py +0 -0
  27. /omlish/specs/irc/{format → protocol}/nuh.py +0 -0
  28. /omlish/specs/irc/{format → protocol}/parsing.py +0 -0
  29. /omlish/specs/irc/{format → protocol}/rendering.py +0 -0
  30. /omlish/specs/irc/{format → protocol}/tags.py +0 -0
  31. /omlish/specs/irc/{format → protocol}/utils.py +0 -0
  32. {omlish-0.0.0.dev222.dist-info → omlish-0.0.0.dev224.dist-info}/LICENSE +0 -0
  33. {omlish-0.0.0.dev222.dist-info → omlish-0.0.0.dev224.dist-info}/WHEEL +0 -0
  34. {omlish-0.0.0.dev222.dist-info → omlish-0.0.0.dev224.dist-info}/entry_points.txt +0 -0
  35. {omlish-0.0.0.dev222.dist-info → omlish-0.0.0.dev224.dist-info}/top_level.txt +0 -0
omlish/__about__.py CHANGED
@@ -1,5 +1,5 @@
1
- __version__ = '0.0.0.dev222'
2
- __revision__ = '6f15b0f3bde72b207c93e58623da4bb444ff4003'
1
+ __version__ = '0.0.0.dev224'
2
+ __revision__ = '0980d15b9ba0f06f68cf7bdb68fbc8626696075f'
3
3
 
4
4
 
5
5
  #
@@ -96,14 +96,14 @@ class Project(ProjectBase):
96
96
  # 'mysqlclient ~= 2.2',
97
97
 
98
98
  'aiomysql ~= 0.2',
99
- 'aiosqlite ~= 0.20',
99
+ 'aiosqlite ~= 0.21',
100
100
  'asyncpg ~= 0.30',
101
101
 
102
102
  'apsw ~= 3.47',
103
103
 
104
104
  'sqlean.py ~= 3.45',
105
105
 
106
- 'duckdb ~= 1.1',
106
+ 'duckdb ~= 1.2',
107
107
  ],
108
108
 
109
109
  'testing': [
@@ -3,7 +3,6 @@
3
3
  import asyncio.base_subprocess
4
4
  import asyncio.subprocess
5
5
  import contextlib
6
- import dataclasses as dc
7
6
  import functools
8
7
  import logging
9
8
  import subprocess
@@ -12,6 +11,8 @@ import typing as ta
12
11
 
13
12
  from ...lite.check import check
14
13
  from ...subprocesses import AbstractAsyncSubprocesses
14
+ from ...subprocesses import SubprocessRun
15
+ from ...subprocesses import SubprocessRunOutput
15
16
  from .timeouts import asyncio_maybe_timeout
16
17
 
17
18
 
@@ -178,41 +179,32 @@ class AsyncioSubprocesses(AbstractAsyncSubprocesses):
178
179
 
179
180
  #
180
181
 
181
- @dc.dataclass(frozen=True)
182
- class RunOutput:
183
- proc: asyncio.subprocess.Process
184
- stdout: ta.Optional[bytes]
185
- stderr: ta.Optional[bytes]
182
+ async def run_(self, run: SubprocessRun) -> SubprocessRunOutput[asyncio.subprocess.Process]:
183
+ kwargs = dict(run.kwargs or {})
186
184
 
187
- async def run(
188
- self,
189
- *cmd: str,
190
- input: ta.Any = None, # noqa
191
- timeout: ta.Optional[float] = None,
192
- check: bool = False, # noqa
193
- capture_output: ta.Optional[bool] = None,
194
- **kwargs: ta.Any,
195
- ) -> RunOutput:
196
- if capture_output:
185
+ if run.capture_output:
197
186
  kwargs.setdefault('stdout', subprocess.PIPE)
198
187
  kwargs.setdefault('stderr', subprocess.PIPE)
199
188
 
200
189
  proc: asyncio.subprocess.Process
201
- async with self.popen(*cmd, **kwargs) as proc:
202
- stdout, stderr = await self.communicate(proc, input, timeout)
190
+ async with self.popen(*run.cmd, **kwargs) as proc:
191
+ stdout, stderr = await self.communicate(proc, run.input, run.timeout)
203
192
 
204
193
  if check and proc.returncode:
205
194
  raise subprocess.CalledProcessError(
206
195
  proc.returncode,
207
- cmd,
196
+ run.cmd,
208
197
  output=stdout,
209
198
  stderr=stderr,
210
199
  )
211
200
 
212
- return self.RunOutput(
213
- proc,
214
- stdout,
215
- stderr,
201
+ return SubprocessRunOutput(
202
+ proc=proc,
203
+
204
+ returncode=check.isinstance(proc.returncode, int),
205
+
206
+ stdout=stdout,
207
+ stderr=stderr,
216
208
  )
217
209
 
218
210
  #
omlish/dynamic.py CHANGED
@@ -170,7 +170,7 @@ class Binding(ta.Generic[T]):
170
170
  while lag_frame is not None:
171
171
  for cur_depth in range(_MAX_HOIST_DEPTH + 1):
172
172
  if lag_frame is None:
173
- break # type: ignore
173
+ break
174
174
  try:
175
175
  lag_hoist = _HOISTED_CODE_DEPTH[lag_frame.f_code]
176
176
  except KeyError:
@@ -49,7 +49,7 @@ class GenMachine(ta.Generic[I, O]):
49
49
  @property
50
50
  def state(self) -> str | None:
51
51
  if self._gen is not None:
52
- return self._gen.gi_code.co_qualname
52
+ return self._gen.gi_code.co_qualname # type: ignore[attr-defined]
53
53
  return None
54
54
 
55
55
  #
omlish/http/coro/fdio.py CHANGED
@@ -33,7 +33,7 @@ class CoroHttpServerConnectionFdioHandler(SocketFdioHandler):
33
33
  self._log_handler = log_handler
34
34
 
35
35
  self._read_buf = ReadableListBuffer()
36
- self._write_buf: IncrementalWriteBuffer | None = None
36
+ self._write_buf: ta.Optional[IncrementalWriteBuffer] = None
37
37
 
38
38
  self._coro_srv = CoroHttpServer(
39
39
  addr,
@@ -41,7 +41,7 @@ class CoroHttpServerConnectionFdioHandler(SocketFdioHandler):
41
41
  )
42
42
  self._srv_coro: ta.Optional[ta.Generator[CoroHttpServer.Io, ta.Optional[bytes], None]] = self._coro_srv.coro_handle() # noqa
43
43
 
44
- self._cur_io: CoroHttpServer.Io | None = None
44
+ self._cur_io: ta.Optional[CoroHttpServer.Io] = None
45
45
  self._next_io()
46
46
 
47
47
  #
@@ -49,7 +49,7 @@ class CoroHttpServerConnectionFdioHandler(SocketFdioHandler):
49
49
  def _next_io(self) -> None: # noqa
50
50
  coro = check.not_none(self._srv_coro)
51
51
 
52
- d: bytes | None = None
52
+ d: ta.Optional[bytes] = None
53
53
  o = self._cur_io
54
54
  while True:
55
55
  if o is None:
@@ -496,7 +496,7 @@ class CoroHttpServer:
496
496
  if isinstance(parsed, ParseHttpRequestError):
497
497
  err = self._build_error(
498
498
  parsed.code,
499
- *parsed.message,
499
+ *([parsed.message] if isinstance(parsed.message, str) else parsed.message),
500
500
  version=parsed.version,
501
501
  )
502
502
  yield self.ErrorLogIo(err)
omlish/http/handlers.py CHANGED
@@ -3,6 +3,7 @@
3
3
  import abc
4
4
  import dataclasses as dc
5
5
  import http.server
6
+ import logging
6
7
  import typing as ta
7
8
 
8
9
  from ..sockets.addresses import SocketAddress
@@ -60,3 +61,67 @@ class HttpHandler_(abc.ABC): # noqa
60
61
  @abc.abstractmethod
61
62
  def __call__(self, req: HttpHandlerRequest) -> HttpHandlerResponse:
62
63
  raise NotImplementedError
64
+
65
+
66
+ ##
67
+
68
+
69
+ @dc.dataclass(frozen=True)
70
+ class LoggingHttpHandler(HttpHandler_):
71
+ handler: HttpHandler
72
+ log: logging.Logger
73
+ level: int = logging.DEBUG
74
+
75
+ def __call__(self, req: HttpHandlerRequest) -> HttpHandlerResponse:
76
+ self.log.log(self.level, '%r', req)
77
+ resp = self.handler(req)
78
+ self.log.log(self.level, '%r', resp)
79
+ return resp
80
+
81
+
82
+ ##
83
+
84
+
85
+ @dc.dataclass(frozen=True)
86
+ class BytesResponseHttpHandler(HttpHandler_):
87
+ data: bytes
88
+
89
+ status: ta.Union[http.HTTPStatus, int] = 200
90
+ content_type: ta.Optional[str] = 'application/octet-stream'
91
+ headers: ta.Optional[ta.Mapping[str, str]] = None
92
+ close_connection: bool = True
93
+
94
+ def __call__(self, req: HttpHandlerRequest) -> HttpHandlerResponse:
95
+ return HttpHandlerResponse(
96
+ status=self.status,
97
+ headers={
98
+ **({'Content-Type': self.content_type} if self.content_type else {}),
99
+ 'Content-Length': str(len(self.data)),
100
+ **(self.headers or {}),
101
+ },
102
+ data=self.data,
103
+ close_connection=self.close_connection,
104
+ )
105
+
106
+
107
+ @dc.dataclass(frozen=True)
108
+ class StringResponseHttpHandler(HttpHandler_):
109
+ data: str
110
+
111
+ status: ta.Union[http.HTTPStatus, int] = 200
112
+ content_type: ta.Optional[str] = 'text/plain; charset=utf-8'
113
+ headers: ta.Optional[ta.Mapping[str, str]] = None
114
+ close_connection: bool = True
115
+
116
+ def __call__(self, req: HttpHandlerRequest) -> HttpHandlerResponse:
117
+ data = self.data.encode('utf-8')
118
+ return HttpHandlerResponse(
119
+ status=self.status,
120
+ headers={
121
+ **({'Content-Type': self.content_type} if self.content_type else {}),
122
+ 'Content-Length': str(len(data)),
123
+ **(self.headers or {}),
124
+ },
125
+ data=data,
126
+ close_connection=self.close_connection,
127
+ )
omlish/http/parsing.py CHANGED
@@ -246,6 +246,8 @@ class HttpRequestParser:
246
246
 
247
247
  #
248
248
 
249
+ _TLS_HANDSHAKE_PREFIX = b'\x16'
250
+
249
251
  def coro_parse(self) -> ta.Generator[int, bytes, ParseHttpRequestResult]:
250
252
  raw_request_line = yield self._max_line + 1
251
253
 
@@ -284,6 +286,17 @@ class HttpRequestParser:
284
286
  if not raw_request_line:
285
287
  return EmptyParsedHttpResult(**result_kwargs())
286
288
 
289
+ # Detect TLS
290
+
291
+ if raw_request_line.startswith(self._TLS_HANDSHAKE_PREFIX):
292
+ return ParseHttpRequestError(
293
+ code=http.HTTPStatus.BAD_REQUEST,
294
+ message='Bad request version (probable TLS handshake)',
295
+ **result_kwargs(),
296
+ )
297
+
298
+ # Decode line
299
+
287
300
  request_line = raw_request_line.decode('iso-8859-1').rstrip('\r\n')
288
301
 
289
302
  # Split words
omlish/lite/marshal.py CHANGED
@@ -422,18 +422,34 @@ class ObjMarshalerManager:
422
422
  return reg
423
423
 
424
424
  if abc.ABC in ty.__bases__:
425
- impls = [ity for ity in deep_subclasses(ty) if abc.ABC not in ity.__bases__] # type: ignore
426
- if all(ity.__qualname__.endswith(ty.__name__) for ity in impls):
427
- ins = {ity: snake_case(ity.__qualname__[:-len(ty.__name__)]) for ity in impls}
428
- else:
429
- ins = {ity: ity.__qualname__ for ity in impls}
425
+ tn = ty.__name__
426
+ impls: ta.List[ta.Tuple[type, str]] = [ # type: ignore[var-annotated]
427
+ (ity, ity.__name__)
428
+ for ity in deep_subclasses(ty)
429
+ if abc.ABC not in ity.__bases__
430
+ ]
431
+
432
+ if all(itn.endswith(tn) for _, itn in impls):
433
+ impls = [
434
+ (ity, snake_case(itn[:-len(tn)]))
435
+ for ity, itn in impls
436
+ ]
437
+
438
+ dupe_tns = sorted(
439
+ dn
440
+ for dn, dc in collections.Counter(itn for _, itn in impls).items()
441
+ if dc > 1
442
+ )
443
+ if dupe_tns:
444
+ raise KeyError(f'Duplicate impl names for {ty}: {dupe_tns}')
445
+
430
446
  return PolymorphicObjMarshaler.of([
431
447
  PolymorphicObjMarshaler.Impl(
432
448
  ity,
433
449
  itn,
434
450
  rec(ity),
435
451
  )
436
- for ity, itn in ins.items()
452
+ for ity, itn in impls
437
453
  ])
438
454
 
439
455
  if issubclass(ty, enum.Enum):
omlish/lite/timing.py ADDED
@@ -0,0 +1,8 @@
1
+ from ..logs.timing import LogTimingContext
2
+ from ..logs.timing import log_timing_context
3
+ from .logs import log
4
+
5
+
6
+ LogTimingContext.DEFAULT_LOG = log
7
+
8
+ log_timing_context = log_timing_context # noqa
omlish/logs/timing.py ADDED
@@ -0,0 +1,58 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import logging
4
+ import time
5
+ import typing as ta
6
+
7
+
8
+ ##
9
+
10
+
11
+ class LogTimingContext:
12
+ DEFAULT_LOG: ta.ClassVar[ta.Optional[logging.Logger]] = None
13
+
14
+ class _NOT_SPECIFIED: # noqa
15
+ def __new__(cls, *args, **kwargs): # noqa
16
+ raise TypeError
17
+
18
+ def __init__(
19
+ self,
20
+ description: str,
21
+ *,
22
+ log: ta.Union[logging.Logger, ta.Type[_NOT_SPECIFIED], None] = _NOT_SPECIFIED, # noqa
23
+ level: int = logging.DEBUG,
24
+ ) -> None:
25
+ super().__init__()
26
+
27
+ self._description = description
28
+ if log is self._NOT_SPECIFIED:
29
+ log = self.DEFAULT_LOG # noqa
30
+ self._log: ta.Optional[logging.Logger] = log # type: ignore
31
+ self._level = level
32
+
33
+ def set_description(self, description: str) -> 'LogTimingContext':
34
+ self._description = description
35
+ return self
36
+
37
+ _begin_time: float
38
+ _end_time: float
39
+
40
+ def __enter__(self) -> 'LogTimingContext':
41
+ self._begin_time = time.time()
42
+
43
+ if self._log is not None:
44
+ self._log.log(self._level, f'Begin : {self._description}') # noqa
45
+
46
+ return self
47
+
48
+ def __exit__(self, exc_type, exc_val, exc_tb):
49
+ self._end_time = time.time()
50
+
51
+ if self._log is not None:
52
+ self._log.log(
53
+ self._level,
54
+ f'End : {self._description} - {self._end_time - self._begin_time:0.2f} s elapsed',
55
+ )
56
+
57
+
58
+ log_timing_context = LogTimingContext
omlish/secrets/tempssl.py CHANGED
@@ -1,10 +1,14 @@
1
1
  # @omlish-lite
2
2
  # ruff: noqa: UP006 UP007
3
+ import dataclasses as dc
3
4
  import os.path
4
- import subprocess
5
5
  import tempfile
6
6
  import typing as ta
7
7
 
8
+ from ..lite.cached import cached_nullary
9
+ from ..subprocesses import SubprocessRun
10
+ from ..subprocesses import SubprocessRunOutput
11
+ from ..subprocesses import subprocesses
8
12
  from .ssl import SslCert
9
13
 
10
14
 
@@ -13,11 +17,15 @@ class TempSslCert(ta.NamedTuple):
13
17
  temp_dir: str
14
18
 
15
19
 
16
- def generate_temp_localhost_ssl_cert() -> TempSslCert:
17
- temp_dir = tempfile.mkdtemp()
20
+ @dc.dataclass(frozen=True)
21
+ class TempSslCertGenerator:
22
+ @cached_nullary
23
+ def temp_dir(self) -> str:
24
+ return tempfile.mkdtemp()
18
25
 
19
- proc = subprocess.run(
20
- [
26
+ @cached_nullary
27
+ def make_run(self) -> SubprocessRun:
28
+ return SubprocessRun.of(
21
29
  'openssl',
22
30
  'req',
23
31
  '-x509',
@@ -32,19 +40,33 @@ def generate_temp_localhost_ssl_cert() -> TempSslCert:
32
40
 
33
41
  '-subj', '/CN=localhost',
34
42
  '-addext', 'subjectAltName = DNS:localhost,IP:127.0.0.1',
35
- ],
36
- cwd=temp_dir,
37
- capture_output=True,
38
- check=False,
39
- )
40
-
41
- if proc.returncode:
42
- raise RuntimeError(f'Failed to generate temp ssl cert: {proc.stderr=}')
43
-
44
- return TempSslCert(
45
- SslCert(
46
- key_file=os.path.join(temp_dir, 'key.pem'),
47
- cert_file=os.path.join(temp_dir, 'cert.pem'),
48
- ),
49
- temp_dir,
50
- )
43
+
44
+ cwd=self.temp_dir(),
45
+ capture_output=True,
46
+ check=False,
47
+ )
48
+
49
+ def handle_run_output(self, proc: SubprocessRunOutput) -> TempSslCert:
50
+ if proc.returncode:
51
+ raise RuntimeError(f'Failed to generate temp ssl cert: {proc.stderr=}')
52
+
53
+ key_file = os.path.join(self.temp_dir(), 'key.pem')
54
+ cert_file = os.path.join(self.temp_dir(), 'cert.pem')
55
+ for file in [key_file, cert_file]:
56
+ if not os.path.isfile(file):
57
+ raise RuntimeError(f'Failed to generate temp ssl cert (file not found): {file}')
58
+
59
+ return TempSslCert(
60
+ SslCert(
61
+ key_file=key_file,
62
+ cert_file=cert_file,
63
+ ),
64
+ temp_dir=self.temp_dir(),
65
+ )
66
+
67
+ def run(self) -> TempSslCert:
68
+ return self.handle_run_output(subprocesses.run_(self.make_run()))
69
+
70
+
71
+ def generate_temp_localhost_ssl_cert() -> TempSslCert:
72
+ return TempSslCertGenerator().run()
File without changes
@@ -0,0 +1,49 @@
1
+ import typing as ta
2
+
3
+ from .... import check
4
+ from .... import dataclasses as dc
5
+ from ....funcs import pairs as fps
6
+ from ..numerics import numerics as nr
7
+ from .formats import MessageFormat
8
+ from .formats import MessageParamsUnpacker
9
+
10
+
11
+ ##
12
+
13
+
14
+ class Message(dc.Case):
15
+ FORMAT: ta.ClassVar[MessageFormat]
16
+ REPLIES: ta.ClassVar[ta.Collection[nr.NumericReply]] = ()
17
+
18
+
19
+ ##
20
+
21
+
22
+ def list_pair_params_unpacker(
23
+ kwarg: str,
24
+ key_param: str,
25
+ value_param: str,
26
+ ) -> MessageParamsUnpacker:
27
+ def forward(params: ta.Mapping[str, str]) -> ta.Mapping[str, ta.Any]:
28
+ out: dict = dict(params)
29
+ ks = out.pop(key_param)
30
+ vs = out.pop(value_param, None)
31
+ if vs is not None:
32
+ out[kwarg] = list(zip(ks, vs, strict=True))
33
+ else:
34
+ out[kwarg] = ks
35
+ return out
36
+
37
+ def backward(kwargs: ta.Mapping[str, ta.Any]) -> ta.Mapping[str, str]:
38
+ out: dict = dict(kwargs)
39
+ ts = out.pop(kwarg)
40
+ is_ts = check.single({isinstance(e, tuple) for e in ts})
41
+ if is_ts:
42
+ ks, vs = zip(*ts)
43
+ out[key_param] = ks
44
+ out[value_param] = vs
45
+ else:
46
+ out[key_param] = ts
47
+ return out
48
+
49
+ return fps.of(forward, backward)
@@ -0,0 +1,92 @@
1
+ import enum
2
+ import typing as ta
3
+
4
+ from .... import check
5
+ from .... import dataclasses as dc
6
+ from .... import lang
7
+ from ....funcs import pairs as fps
8
+
9
+
10
+ MessageParamsUnpacker: ta.TypeAlias = fps.FnPair[
11
+ ta.Mapping[str, str], # params
12
+ ta.Mapping[str, ta.Any], # kwargs
13
+ ]
14
+
15
+
16
+ ##
17
+
18
+
19
+ class MessageFormat(dc.Frozen, final=True):
20
+ name: str
21
+
22
+ class Param(dc.Case):
23
+ @classmethod
24
+ def of(cls, obj: ta.Any) -> 'MessageFormat.Param':
25
+ if isinstance(obj, MessageFormat.Param):
26
+ return obj
27
+
28
+ elif isinstance(obj, str):
29
+ s = check.non_empty_str(obj)
30
+
31
+ optional = False
32
+ if s.startswith('?'):
33
+ optional = True
34
+ s = s[1:]
35
+
36
+ arity = MessageFormat.KwargParam.Arity.SINGLE
37
+ if s.startswith('*'):
38
+ arity = MessageFormat.KwargParam.Arity.VARIADIC
39
+ s = s[1:]
40
+
41
+ elif s.startswith(','):
42
+ arity = MessageFormat.KwargParam.Arity.COMMA_LIST
43
+ s = s[1:]
44
+
45
+ return MessageFormat.KwargParam(
46
+ s,
47
+ optional=optional,
48
+ arity=arity,
49
+ )
50
+
51
+ else:
52
+ raise TypeError(obj)
53
+
54
+ class KwargParam(Param):
55
+ name: str = dc.xfield(validate=lang.is_ident)
56
+
57
+ optional: bool = False
58
+
59
+ class Arity(enum.Enum):
60
+ SINGLE = enum.auto() # <foo>
61
+ VARIADIC = enum.auto() # <foo>{ <foo>}
62
+ COMMA_LIST = enum.auto() # <foo>{,<foo>}
63
+
64
+ arity: Arity = Arity.SINGLE
65
+
66
+ class LiteralParam(Param):
67
+ text: str
68
+
69
+ params: ta.Sequence[Param]
70
+
71
+ _: dc.KW_ONLY
72
+
73
+ unpack_params: MessageParamsUnpacker | None = None
74
+
75
+ @dc.init
76
+ def _validate_params(self) -> None:
77
+ kws = [p for p in self.params if isinstance(p, MessageFormat.KwargParam)]
78
+ check.unique(p.name for p in kws)
79
+ check.state(all(p.arity is not MessageFormat.KwargParam.Arity.VARIADIC for p in kws[:-1]))
80
+
81
+ @classmethod
82
+ def of(
83
+ cls,
84
+ name: str,
85
+ *params: ta.Any,
86
+ **kwargs: ta.Any,
87
+ ) -> 'MessageFormat':
88
+ return cls(
89
+ name,
90
+ [MessageFormat.Param.of(p) for p in params],
91
+ **kwargs,
92
+ )