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.
- omlish/__about__.py +2 -2
- omlish/algorithm/__init__.py +0 -0
- omlish/algorithm/all.py +13 -0
- omlish/algorithm/distribute.py +46 -0
- omlish/algorithm/toposort.py +26 -0
- omlish/algorithm/unify.py +31 -0
- omlish/collections/__init__.py +0 -2
- omlish/collections/utils.py +0 -46
- omlish/docker/oci/building.py +122 -0
- omlish/docker/oci/data.py +62 -8
- omlish/docker/oci/datarefs.py +98 -0
- omlish/docker/oci/loading.py +120 -0
- omlish/docker/oci/media.py +44 -14
- omlish/docker/oci/repositories.py +72 -0
- omlish/graphs/trees.py +2 -1
- omlish/http/coro/server.py +42 -33
- omlish/http/{simple.py → coro/simple.py} +17 -17
- omlish/specs/irc/__init__.py +0 -0
- omlish/specs/irc/format/LICENSE +11 -0
- omlish/specs/irc/format/__init__.py +61 -0
- omlish/specs/irc/format/consts.py +6 -0
- omlish/specs/irc/format/errors.py +30 -0
- omlish/specs/irc/format/message.py +18 -0
- omlish/specs/irc/format/nuh.py +52 -0
- omlish/specs/irc/format/parsing.py +155 -0
- omlish/specs/irc/format/rendering.py +150 -0
- omlish/specs/irc/format/tags.py +99 -0
- omlish/specs/irc/format/utils.py +27 -0
- omlish/specs/irc/numerics/__init__.py +0 -0
- omlish/specs/irc/numerics/formats.py +94 -0
- omlish/specs/irc/numerics/numerics.py +808 -0
- omlish/specs/irc/numerics/types.py +59 -0
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/METADATA +1 -1
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/RECORD +38 -14
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/LICENSE +0 -0
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/WHEEL +0 -0
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/entry_points.txt +0 -0
- {omlish-0.0.0.dev220.dist-info → omlish-0.0.0.dev221.dist-info}/top_level.txt +0 -0
omlish/docker/oci/media.py
CHANGED
@@ -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
|
-
|
19
|
-
|
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(
|
55
|
-
class OciMediaDescriptor
|
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(
|
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(
|
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(
|
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(
|
198
|
+
ts = list(alg.mut_toposort(tsd))
|
198
199
|
root = d(check.single(ts[0]))
|
199
200
|
|
200
201
|
return cls(
|
omlish/http/coro/server.py
CHANGED
@@ -421,50 +421,59 @@ class CoroHttpServer:
|
|
421
421
|
#
|
422
422
|
|
423
423
|
def coro_handle(self) -> ta.Generator[Io, ta.Optional[bytes], None]:
|
424
|
-
|
425
|
-
gen = self.coro_handle_one()
|
424
|
+
return self._coro_run_handler(self._coro_handle_one())
|
426
425
|
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
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
|
-
|
436
|
-
|
442
|
+
elif isinstance(o, self.AnyReadIo):
|
443
|
+
i = check.isinstance((yield o), bytes)
|
437
444
|
|
438
|
-
|
439
|
-
|
445
|
+
elif isinstance(o, self._Response):
|
446
|
+
i = None
|
440
447
|
|
441
|
-
|
442
|
-
|
443
|
-
|
448
|
+
r = self._preprocess_response(o)
|
449
|
+
hb = self._build_response_head_bytes(r)
|
450
|
+
check.none((yield self.WriteIo(hb)))
|
444
451
|
|
445
|
-
|
446
|
-
|
452
|
+
for b in self._yield_response_data(r):
|
453
|
+
yield self.WriteIo(b)
|
447
454
|
|
448
|
-
|
449
|
-
|
455
|
+
o.close()
|
456
|
+
if o.close_connection:
|
457
|
+
break
|
458
|
+
o = None
|
450
459
|
|
451
|
-
|
452
|
-
|
460
|
+
else:
|
461
|
+
raise TypeError(o) # noqa
|
453
462
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
463
|
+
try:
|
464
|
+
o = gen.send(i)
|
465
|
+
except EOFError:
|
466
|
+
return
|
467
|
+
except StopIteration:
|
468
|
+
break
|
460
469
|
|
461
|
-
|
462
|
-
|
463
|
-
|
470
|
+
except Exception: # noqa
|
471
|
+
if hasattr(o, 'close'):
|
472
|
+
o.close()
|
464
473
|
|
465
|
-
|
474
|
+
raise
|
466
475
|
|
467
|
-
def
|
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
|
9
|
-
from
|
10
|
-
from
|
11
|
-
from
|
12
|
-
from
|
13
|
-
from
|
14
|
-
from
|
15
|
-
from
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from
|
19
|
-
from
|
20
|
-
from
|
21
|
-
from
|
22
|
-
from
|
23
|
-
from .
|
24
|
-
from .
|
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,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
|
+
)
|