omlish 0.0.0.dev220__py3-none-any.whl → 0.0.0.dev221__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 (38) hide show
  1. omlish/__about__.py +2 -2
  2. omlish/algorithm/__init__.py +0 -0
  3. omlish/algorithm/all.py +13 -0
  4. omlish/algorithm/distribute.py +46 -0
  5. omlish/algorithm/toposort.py +26 -0
  6. omlish/algorithm/unify.py +31 -0
  7. omlish/collections/__init__.py +0 -2
  8. omlish/collections/utils.py +0 -46
  9. omlish/docker/oci/building.py +122 -0
  10. omlish/docker/oci/data.py +62 -8
  11. omlish/docker/oci/datarefs.py +98 -0
  12. omlish/docker/oci/loading.py +120 -0
  13. omlish/docker/oci/media.py +44 -14
  14. omlish/docker/oci/repositories.py +72 -0
  15. omlish/graphs/trees.py +2 -1
  16. omlish/http/coro/server.py +42 -33
  17. omlish/http/{simple.py → coro/simple.py} +17 -17
  18. omlish/specs/irc/__init__.py +0 -0
  19. omlish/specs/irc/format/LICENSE +11 -0
  20. omlish/specs/irc/format/__init__.py +61 -0
  21. omlish/specs/irc/format/consts.py +6 -0
  22. omlish/specs/irc/format/errors.py +30 -0
  23. omlish/specs/irc/format/message.py +18 -0
  24. omlish/specs/irc/format/nuh.py +52 -0
  25. omlish/specs/irc/format/parsing.py +155 -0
  26. omlish/specs/irc/format/rendering.py +150 -0
  27. omlish/specs/irc/format/tags.py +99 -0
  28. omlish/specs/irc/format/utils.py +27 -0
  29. omlish/specs/irc/numerics/__init__.py +0 -0
  30. omlish/specs/irc/numerics/formats.py +94 -0
  31. omlish/specs/irc/numerics/numerics.py +808 -0
  32. omlish/specs/irc/numerics/types.py +59 -0
  33. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/METADATA +1 -1
  34. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/RECORD +38 -14
  35. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/LICENSE +0 -0
  36. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/WHEEL +0 -0
  37. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/entry_points.txt +0 -0
  38. {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/top_level.txt +0 -0
@@ -8,18 +8,29 @@ from ...lite.check import check
8
8
  from ...lite.marshal import OBJ_MARSHALER_FIELD_KEY
9
9
  from ...lite.marshal import OBJ_MARSHALER_OMIT_IF_NONE
10
10
  from ...lite.marshal import unmarshal_obj
11
- from .data import OciDataclass
12
11
  from .data import OciImageConfig
12
+ from .data import OciImageLayer
13
13
 
14
14
 
15
15
  ##
16
16
 
17
17
 
18
- @dc.dataclass(frozen=True)
19
- class OciMediaDataclass(OciDataclass, abc.ABC): # noqa
18
+ OCI_MEDIA_FIELDS: ta.Collection[str] = frozenset([
19
+ 'schema_version',
20
+ 'media_type',
21
+ ])
22
+
23
+
24
+ @dc.dataclass()
25
+ class OciMediaDataclass(abc.ABC): # noqa
20
26
  SCHEMA_VERSION: ta.ClassVar[int]
21
27
  MEDIA_TYPE: ta.ClassVar[str]
22
28
 
29
+ def __init_subclass__(cls, **kwargs: ta.Any) -> None:
30
+ super().__init_subclass__(**kwargs)
31
+ for a in OCI_MEDIA_FIELDS:
32
+ check.in_(a, cls.__dict__)
33
+
23
34
 
24
35
  _REGISTERED_OCI_MEDIA_DATACLASSES: ta.Dict[str, ta.Type[OciMediaDataclass]] = {}
25
36
 
@@ -48,11 +59,11 @@ def unmarshal_oci_media_dataclass(
48
59
  return unmarshal_obj(dct, cls)
49
60
 
50
61
 
51
- #
62
+ ##
52
63
 
53
64
 
54
- @dc.dataclass(frozen=True)
55
- class OciMediaDescriptor(OciDataclass):
65
+ @dc.dataclass()
66
+ class OciMediaDescriptor:
56
67
  """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/descriptor.md#properties""" # noqa
57
68
 
58
69
  media_type: str = dc.field(metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
@@ -71,8 +82,11 @@ class OciMediaDescriptor(OciDataclass):
71
82
  platform: ta.Optional[ta.Mapping[str, ta.Any]] = dc.field(default=None, metadata={OBJ_MARSHALER_OMIT_IF_NONE: True}) # noqa
72
83
 
73
84
 
85
+ ##
86
+
87
+
74
88
  @_register_oci_media_dataclass
75
- @dc.dataclass(frozen=True)
89
+ @dc.dataclass()
76
90
  class OciMediaImageIndex(OciMediaDataclass):
77
91
  """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/image-index.md"""
78
92
 
@@ -91,18 +105,16 @@ class OciMediaImageIndex(OciMediaDataclass):
91
105
  media_type: str = dc.field(default=MEDIA_TYPE, metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
92
106
 
93
107
 
108
+ #
109
+
110
+
94
111
  @_register_oci_media_dataclass
95
- @dc.dataclass(frozen=True)
112
+ @dc.dataclass()
96
113
  class OciMediaImageManifest(OciMediaDataclass):
97
114
  """https://github.com/opencontainers/image-spec/blob/92353b0bee778725c617e7d57317b568a7796bd0/manifest.md"""
98
115
 
99
116
  config: OciMediaDescriptor # -> OciMediaImageConfig
100
117
 
101
- # MEDIA_TYPES: ta.ClassVar[ta.Mapping[str, str]] = {
102
- # 'TAR': 'application/vnd.oci.image.layer.v1.tar',
103
- # 'TAR_GZIP': 'application/vnd.oci.image.layer.v1.tar+gzip',
104
- # 'TAR_ZSTD': 'application/vnd.oci.image.layer.v1.tar+zstd',
105
- # }
106
118
  layers: ta.Sequence[OciMediaDescriptor]
107
119
 
108
120
  #
@@ -114,8 +126,26 @@ class OciMediaImageManifest(OciMediaDataclass):
114
126
  media_type: str = dc.field(default=MEDIA_TYPE, metadata={OBJ_MARSHALER_FIELD_KEY: 'mediaType'})
115
127
 
116
128
 
129
+ #
130
+
131
+
132
+ OCI_IMAGE_LAYER_KIND_MEDIA_TYPES: ta.Mapping[OciImageLayer.Kind, str] = {
133
+ OciImageLayer.Kind.TAR: 'application/vnd.oci.image.layer.v1.tar',
134
+ OciImageLayer.Kind.TAR_GZIP: 'application/vnd.oci.image.layer.v1.tar+gzip',
135
+ OciImageLayer.Kind.TAR_ZSTD: 'application/vnd.oci.image.layer.v1.tar+zstd',
136
+ }
137
+
138
+ OCI_IMAGE_LAYER_KIND_MEDIA_TYPES_: ta.Mapping[str, OciImageLayer.Kind] = {
139
+ v: k
140
+ for k, v in OCI_IMAGE_LAYER_KIND_MEDIA_TYPES.items()
141
+ }
142
+
143
+
144
+ #
145
+
146
+
117
147
  @_register_oci_media_dataclass
118
- @dc.dataclass(frozen=True)
148
+ @dc.dataclass()
119
149
  class OciMediaImageConfig(OciImageConfig, OciMediaDataclass):
120
150
  SCHEMA_VERSION: ta.ClassVar[int] = 2
121
151
  schema_version: int = dc.field(default=SCHEMA_VERSION, metadata={OBJ_MARSHALER_FIELD_KEY: 'schemaVersion'})
@@ -0,0 +1,72 @@
1
+ # ruff: noqa: UP006 UP007
2
+ # @omlish-lite
3
+ import abc
4
+ import os.path
5
+ import typing as ta
6
+
7
+ from ...lite.check import check
8
+ from ...os.paths import is_path_in_dir
9
+ from .datarefs import BytesOciDataRef
10
+ from .datarefs import FileOciDataRef
11
+ from .datarefs import OciDataRef
12
+
13
+
14
+ ##
15
+
16
+
17
+ class OciRepository(abc.ABC):
18
+ @abc.abstractmethod
19
+ def read_blob(self, digest: str) -> bytes:
20
+ raise NotImplementedError
21
+
22
+ @abc.abstractmethod
23
+ def ref_blob(self, digest: str) -> OciDataRef:
24
+ raise NotImplementedError
25
+
26
+
27
+ #
28
+
29
+
30
+ class DirectoryOciRepository(OciRepository):
31
+ def __init__(self, data_dir: str) -> None:
32
+ super().__init__()
33
+
34
+ self._data_dir = check.non_empty_str(data_dir)
35
+
36
+ def read_file(self, path: str) -> bytes:
37
+ full_path = os.path.join(self._data_dir, path)
38
+ check.arg(is_path_in_dir(self._data_dir, full_path))
39
+ with open(full_path, 'rb') as f:
40
+ return f.read()
41
+
42
+ def blob_path(self, digest: str) -> str:
43
+ scheme, value = digest.split(':')
44
+ return os.path.join('blobs', scheme, value)
45
+
46
+ def blob_full_path(self, digest: str) -> str:
47
+ path = self.blob_path(digest)
48
+ full_path = os.path.join(self._data_dir, path)
49
+ check.arg(is_path_in_dir(self._data_dir, full_path))
50
+ return full_path
51
+
52
+ def read_blob(self, digest: str) -> bytes:
53
+ return self.read_file(self.blob_path(digest))
54
+
55
+ def ref_blob(self, digest: str) -> OciDataRef:
56
+ return FileOciDataRef(self.blob_full_path(digest))
57
+
58
+
59
+ #
60
+
61
+
62
+ class DictionaryOciRepository(OciRepository):
63
+ def __init__(self, blobs: ta.Mapping[str, bytes]) -> None:
64
+ super().__init__()
65
+
66
+ self._blobs = blobs
67
+
68
+ def read_blob(self, digest: str) -> bytes:
69
+ return self._blobs[digest]
70
+
71
+ def ref_blob(self, digest: str) -> OciDataRef:
72
+ return BytesOciDataRef(self._blobs[digest])
omlish/graphs/trees.py CHANGED
@@ -10,6 +10,7 @@ from .. import cached
10
10
  from .. import check
11
11
  from .. import collections as col
12
12
  from .. import lang
13
+ from ..algorithm import all as alg
13
14
 
14
15
 
15
16
  T = ta.TypeVar('T')
@@ -194,7 +195,7 @@ class BasicTreeAnalysis(ta.Generic[NodeT]):
194
195
  else:
195
196
  e, d = lang.identity, lang.identity
196
197
  tsd = {e(n): {e(p)} for n, p in parents_by_node.items()}
197
- ts = list(col.mut_toposort(tsd))
198
+ ts = list(alg.mut_toposort(tsd))
198
199
  root = d(check.single(ts[0]))
199
200
 
200
201
  return cls(
@@ -421,50 +421,59 @@ class CoroHttpServer:
421
421
  #
422
422
 
423
423
  def coro_handle(self) -> ta.Generator[Io, ta.Optional[bytes], None]:
424
- while True:
425
- gen = self.coro_handle_one()
424
+ return self._coro_run_handler(self._coro_handle_one())
426
425
 
427
- i: ta.Optional[bytes]
428
- o: ta.Any = next(gen)
429
- while True:
430
- try:
431
- if isinstance(o, self.AnyLogIo):
432
- i = None
433
- yield o
426
+ def _coro_run_handler(
427
+ self,
428
+ gen: ta.Generator[
429
+ ta.Union[AnyLogIo, AnyReadIo, _Response],
430
+ ta.Optional[bytes],
431
+ None,
432
+ ],
433
+ ) -> ta.Generator[Io, ta.Optional[bytes], None]:
434
+ i: ta.Optional[bytes]
435
+ o: ta.Any = next(gen)
436
+ while True:
437
+ try:
438
+ if isinstance(o, self.AnyLogIo):
439
+ i = None
440
+ yield o
434
441
 
435
- elif isinstance(o, self.AnyReadIo):
436
- i = check.isinstance((yield o), bytes)
442
+ elif isinstance(o, self.AnyReadIo):
443
+ i = check.isinstance((yield o), bytes)
437
444
 
438
- elif isinstance(o, self._Response):
439
- i = None
445
+ elif isinstance(o, self._Response):
446
+ i = None
440
447
 
441
- r = self._preprocess_response(o)
442
- hb = self._build_response_head_bytes(r)
443
- check.none((yield self.WriteIo(hb)))
448
+ r = self._preprocess_response(o)
449
+ hb = self._build_response_head_bytes(r)
450
+ check.none((yield self.WriteIo(hb)))
444
451
 
445
- for b in self._yield_response_data(r):
446
- yield self.WriteIo(b)
452
+ for b in self._yield_response_data(r):
453
+ yield self.WriteIo(b)
447
454
 
448
- o.close()
449
- o = None
455
+ o.close()
456
+ if o.close_connection:
457
+ break
458
+ o = None
450
459
 
451
- else:
452
- raise TypeError(o) # noqa
460
+ else:
461
+ raise TypeError(o) # noqa
453
462
 
454
- try:
455
- o = gen.send(i)
456
- except EOFError:
457
- return
458
- except StopIteration:
459
- break
463
+ try:
464
+ o = gen.send(i)
465
+ except EOFError:
466
+ return
467
+ except StopIteration:
468
+ break
460
469
 
461
- except Exception: # noqa
462
- if hasattr(o, 'close'):
463
- o.close()
470
+ except Exception: # noqa
471
+ if hasattr(o, 'close'):
472
+ o.close()
464
473
 
465
- raise
474
+ raise
466
475
 
467
- def coro_handle_one(self) -> ta.Generator[
476
+ def _coro_handle_one(self) -> ta.Generator[
468
477
  ta.Union[AnyLogIo, AnyReadIo, _Response],
469
478
  ta.Optional[bytes],
470
479
  None,
@@ -5,23 +5,23 @@ import contextlib
5
5
  import functools
6
6
  import typing as ta
7
7
 
8
- from ..lite.check import check
9
- from ..sockets.addresses import SocketAndAddress
10
- from ..sockets.bind import CanSocketBinder
11
- from ..sockets.bind import SocketBinder
12
- from ..sockets.server.handlers import ExecutorSocketServerHandler
13
- from ..sockets.server.handlers import SocketHandlerSocketServerHandler
14
- from ..sockets.server.handlers import SocketServerHandler
15
- from ..sockets.server.handlers import SocketWrappingSocketServerHandler
16
- from ..sockets.server.handlers import StandardSocketServerHandler
17
- from ..sockets.server.server import SocketServer
18
- from ..sockets.server.threading import ThreadingSocketServerHandler
19
- from .coro.server import CoroHttpServer
20
- from .coro.server import CoroHttpServerSocketHandler
21
- from .handlers import HttpHandler
22
- from .parsing import HttpRequestParser
23
- from .versions import HttpProtocolVersion
24
- from .versions import HttpProtocolVersions
8
+ from ...lite.check import check
9
+ from ...sockets.addresses import SocketAndAddress
10
+ from ...sockets.bind import CanSocketBinder
11
+ from ...sockets.bind import SocketBinder
12
+ from ...sockets.server.handlers import ExecutorSocketServerHandler
13
+ from ...sockets.server.handlers import SocketHandlerSocketServerHandler
14
+ from ...sockets.server.handlers import SocketServerHandler
15
+ from ...sockets.server.handlers import SocketWrappingSocketServerHandler
16
+ from ...sockets.server.handlers import StandardSocketServerHandler
17
+ from ...sockets.server.server import SocketServer
18
+ from ...sockets.server.threading import ThreadingSocketServerHandler
19
+ from ..handlers import HttpHandler
20
+ from ..parsing import HttpRequestParser
21
+ from ..versions import HttpProtocolVersion
22
+ from ..versions import HttpProtocolVersions
23
+ from .server import CoroHttpServer
24
+ from .server import CoroHttpServerSocketHandler
25
25
 
26
26
 
27
27
  if ta.TYPE_CHECKING:
File without changes
@@ -0,0 +1,11 @@
1
+ Copyright (c) 2016-2021 Daniel Oaks
2
+ Copyright (c) 2018-2021 Shivaram Lingamneni
3
+
4
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted,
5
+ provided that the above copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
8
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
10
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
11
+ THIS SOFTWARE.
@@ -0,0 +1,61 @@
1
+ # Copyright (c) 2016-2021 Daniel Oaks
2
+ # Copyright (c) 2018-2021 Shivaram Lingamneni
3
+ #
4
+ # Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
5
+ # granted, provided that the above copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
8
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
9
+ # INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
10
+ # AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
11
+ # PERFORMANCE OF THIS SOFTWARE.
12
+ #
13
+ # https://github.com/ergochat/irc-go/blob/9beac2d29dc5f998c5a53e5db7a6426d7d083a79/ircmsg/
14
+ from .consts import ( # noqa
15
+ MAX_LEN_CLIENT_TAG_DATA,
16
+ MAX_LEN_SERVER_TAG_DATA,
17
+ MAX_LEN_TAGS,
18
+ MAX_LEN_TAGS_FROM_CLIENT,
19
+ MAX_LEN_TAG_DATA,
20
+ )
21
+
22
+ from .errors import ( # noqa
23
+ BadCharactersError,
24
+ BadParamError,
25
+ CommandMissingError,
26
+ Error,
27
+ InvalidTagContentError,
28
+ LineEmptyError,
29
+ MalformedNuhError,
30
+ TagsTooLongError,
31
+ )
32
+
33
+ from .message import ( # noqa
34
+ Message,
35
+ )
36
+
37
+ from .nuh import ( # noqa
38
+ Nuh,
39
+ )
40
+
41
+ from .parsing import ( # noqa
42
+ ParsedLine,
43
+ parse_line,
44
+ parse_line_,
45
+ parse_line_strict,
46
+ )
47
+
48
+ from .rendering import ( # noqa
49
+ RenderedLine,
50
+ render_line,
51
+ render_line_,
52
+ render_line_strict,
53
+ )
54
+
55
+ from .tags import ( # noqa
56
+ escape_tag_value,
57
+ parse_tags,
58
+ unescape_tag_value,
59
+ validate_tag_name,
60
+ validate_tag_value,
61
+ )
@@ -0,0 +1,6 @@
1
+ # Constants
2
+ MAX_LEN_TAGS = 8191
3
+ MAX_LEN_TAG_DATA = MAX_LEN_TAGS - 2
4
+ MAX_LEN_CLIENT_TAG_DATA = 4094
5
+ MAX_LEN_SERVER_TAG_DATA = 4094
6
+ MAX_LEN_TAGS_FROM_CLIENT = MAX_LEN_CLIENT_TAG_DATA + 2
@@ -0,0 +1,30 @@
1
+ class Error(Exception):
2
+ pass
3
+
4
+
5
+ class LineEmptyError(Error):
6
+ pass
7
+
8
+
9
+ class BadCharactersError(Error):
10
+ pass
11
+
12
+
13
+ class TagsTooLongError(Error):
14
+ pass
15
+
16
+
17
+ class InvalidTagContentError(Error):
18
+ pass
19
+
20
+
21
+ class CommandMissingError(Error):
22
+ pass
23
+
24
+
25
+ class BadParamError(Error):
26
+ pass
27
+
28
+
29
+ class MalformedNuhError(Error):
30
+ pass
@@ -0,0 +1,18 @@
1
+ """
2
+ TODO:
3
+ - https://github.com/ergochat/irctest/blob/master/irctest/irc_utils/message_parser.py ?
4
+ """
5
+ import dataclasses as dc
6
+ import typing as ta
7
+
8
+
9
+ @dc.dataclass(frozen=True)
10
+ class Message:
11
+ source: str | None
12
+ command: str
13
+ params: ta.Sequence[str]
14
+
15
+ force_trailing: bool = False
16
+
17
+ tags: ta.Mapping[str, str] | None = None
18
+ client_only_tags: ta.Mapping[str, str] | None = None
@@ -0,0 +1,52 @@
1
+ import dataclasses as dc
2
+ import typing as ta
3
+
4
+ from .errors import MalformedNuhError
5
+
6
+
7
+ @dc.dataclass(frozen=True)
8
+ class Nuh:
9
+ name: str | None = None
10
+ user: str | None = None
11
+ host: str | None = None
12
+
13
+ @property
14
+ def tuple(self) -> tuple[str | None, ...]:
15
+ return (self.name, self.user, self.host)
16
+
17
+ def __iter__(self) -> ta.Iterator[str | None]:
18
+ return iter(self.tuple)
19
+
20
+ @classmethod
21
+ def parse(cls, inp: str) -> 'Nuh':
22
+ if not inp:
23
+ raise MalformedNuhError
24
+
25
+ host: str | None = None
26
+ host_start = inp.find('@')
27
+ if host_start != -1:
28
+ host = inp[host_start + 1:]
29
+ inp = inp[:host_start]
30
+
31
+ user_start = inp.find('!')
32
+ user: str | None = None
33
+ if user_start != -1:
34
+ user = inp[user_start + 1:]
35
+ inp = inp[:user_start]
36
+
37
+ return cls(
38
+ name=inp or None,
39
+ user=user,
40
+ host=host,
41
+ )
42
+
43
+ @property
44
+ def canonical(self) -> str:
45
+ parts = []
46
+ if (n := self.name) is not None:
47
+ parts.append(n)
48
+ if (u := self.user) is not None:
49
+ parts.append(f'!{u}')
50
+ if (h := self.host) is not None:
51
+ parts.append(f'@{h}')
52
+ return ''.join(parts)
@@ -0,0 +1,155 @@
1
+ import typing as ta
2
+
3
+ from .consts import MAX_LEN_CLIENT_TAG_DATA
4
+ from .consts import MAX_LEN_TAG_DATA
5
+ from .errors import BadCharactersError
6
+ from .errors import LineEmptyError
7
+ from .errors import TagsTooLongError
8
+ from .message import Message
9
+ from .tags import parse_tags
10
+ from .utils import is_ascii
11
+ from .utils import trim_initial_spaces
12
+ from .utils import truncate_utf8_safe
13
+
14
+
15
+ class ParsedLine(ta.NamedTuple):
16
+ message: Message
17
+
18
+ truncated: bool = False
19
+
20
+
21
+ def parse_line_(
22
+ line: str,
23
+ *,
24
+ max_tag_data_length: int | None = None,
25
+ truncate_len: int | None = None,
26
+ ) -> ParsedLine:
27
+ # Remove either \n or \r\n from the end of the line:
28
+ line = line.removesuffix('\n')
29
+ line = line.removesuffix('\r')
30
+
31
+ # Whether we removed them ourselves, or whether they were removed previously, they count against the line limit:
32
+ if truncate_len is not None:
33
+ if truncate_len <= 2:
34
+ raise LineEmptyError
35
+ truncate_len -= 2
36
+
37
+ # Now validate for the 3 forbidden bytes:
38
+ if any(c in line for c in '\x00\n\r'):
39
+ raise BadCharactersError
40
+
41
+ if not line:
42
+ raise LineEmptyError
43
+
44
+ #
45
+
46
+ # Handle tags
47
+ tags: ta.Mapping[str, str] | None = None
48
+ if line.startswith('@'):
49
+ tag_end = line.find(' ')
50
+ if tag_end == -1:
51
+ raise LineEmptyError
52
+ raw_tags = line[1:tag_end]
53
+ if max_tag_data_length is not None and len(raw_tags) > max_tag_data_length:
54
+ raise TagsTooLongError
55
+ tags = parse_tags(raw_tags)
56
+ # Skip over the tags and the separating space
57
+ line = line[tag_end + 1:]
58
+
59
+ #
60
+
61
+ # Truncate if desired
62
+ truncated = False
63
+ if truncate_len is not None and len(line) > truncate_len:
64
+ line = truncate_utf8_safe(line, truncate_len)
65
+ truncated = True
66
+
67
+ line = trim_initial_spaces(line)
68
+
69
+ # Handle source
70
+ source: str | None = None
71
+
72
+ if line.startswith(':'):
73
+ source_end = line.find(' ')
74
+ if source_end == -1:
75
+ raise LineEmptyError
76
+ source = line[1:source_end]
77
+ line = line[source_end + 1:]
78
+
79
+ # Modern: "These message parts, and parameters themselves, are separated by one or more ASCII SPACE characters"
80
+ line = trim_initial_spaces(line)
81
+
82
+ # Handle command
83
+ command_end = line.find(' ')
84
+ param_start = command_end + 1 if command_end != -1 else len(line)
85
+ base_command = line[:command_end] if command_end != -1 else line
86
+
87
+ if not base_command:
88
+ raise LineEmptyError
89
+ # Technically this must be either letters or a 3-digit numeric:
90
+ if not is_ascii(base_command):
91
+ raise BadCharactersError
92
+
93
+ # Normalize command to uppercase:
94
+ command = base_command.upper()
95
+ line = line[param_start:]
96
+
97
+ # Handle parameters
98
+ params: list[str] = []
99
+ while line:
100
+ line = trim_initial_spaces(line)
101
+ if not line:
102
+ break
103
+ # Handle trailing
104
+ if line.startswith(':'):
105
+ params.append(line[1:])
106
+ break
107
+ param_end = line.find(' ')
108
+ if param_end == -1:
109
+ params.append(line)
110
+ break
111
+ params.append(line[:param_end])
112
+ line = line[param_end + 1:]
113
+
114
+ #
115
+
116
+ msg = Message(
117
+ source=source,
118
+ command=command,
119
+ params=params,
120
+ tags=tags,
121
+ )
122
+
123
+ return ParsedLine(
124
+ msg,
125
+ truncated=truncated,
126
+ )
127
+
128
+
129
+ def parse_line(
130
+ line: str,
131
+ *,
132
+ max_tag_data_length: int | None = None,
133
+ truncate_len: int | None = None,
134
+ ) -> Message:
135
+ return parse_line_(
136
+ line,
137
+ max_tag_data_length=max_tag_data_length,
138
+ truncate_len=truncate_len,
139
+ ).message
140
+
141
+
142
+ def parse_line_strict(
143
+ line: str,
144
+ from_client: bool,
145
+ truncate_len: int | None,
146
+ ) -> ParsedLine:
147
+ max_tag_data_length = MAX_LEN_TAG_DATA
148
+ if from_client:
149
+ max_tag_data_length = MAX_LEN_CLIENT_TAG_DATA
150
+
151
+ return parse_line_(
152
+ line,
153
+ max_tag_data_length=max_tag_data_length,
154
+ truncate_len=truncate_len,
155
+ )