omlish 0.0.0.dev220__py3-none-any.whl → 0.0.0.dev221__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
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
+ )