gnobjects 0.0.0.0.1__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.
- gnobjects-0.0.0.0.1/LICENSE +21 -0
- gnobjects-0.0.0.0.1/MANIFEST.in +4 -0
- gnobjects-0.0.0.0.1/PKG-INFO +28 -0
- gnobjects-0.0.0.0.1/gnobjects/LICENSE +28 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects/__init__.py +31 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects/net/_func.py +192 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects/net/_transport_pr_obj.py +294 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects/net/fastcommands.py +165 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects/net/objects.py +955 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects.egg-info/PKG-INFO +28 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects.egg-info/SOURCES.txt +15 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects.egg-info/dependency_links.txt +1 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects.egg-info/requires.txt +2 -0
- gnobjects-0.0.0.0.1/gnobjects/gnobjects.egg-info/top_level.txt +1 -0
- gnobjects-0.0.0.0.1/gnobjects/mmbConfig.json +6 -0
- gnobjects-0.0.0.0.1/setup.cfg +4 -0
- gnobjects-0.0.0.0.1/setup.py +25 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 KeyisB
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gnobjects
|
|
3
|
+
Version: 0.0.0.0.1
|
|
4
|
+
Summary: gnobjects
|
|
5
|
+
Home-page: https://github.com/KeyisB/libs/tree/main/gnobjects
|
|
6
|
+
Author: KeyisB
|
|
7
|
+
Author-email: keyisb.pip@gmail.com
|
|
8
|
+
License: MMB License v1.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/plain
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: KeyisBTools
|
|
15
|
+
Requires-Dist: ast
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: license
|
|
23
|
+
Dynamic: license-file
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
GW and MMB Project libraries
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Copyright (C) 2024 KeyisB. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
|
4
|
+
obtaining a copy of this software and associated
|
|
5
|
+
1. Copying, modification, merging, publishing, distribution,
|
|
6
|
+
sublicensing, and/or selling copies of the Software are
|
|
7
|
+
strictly prohibited.documentation
|
|
8
|
+
files (the "Software"), to use the Software exclusively for
|
|
9
|
+
projects related to the GN or GW systems, including personal,
|
|
10
|
+
educational, and commercial purposes, subject to the following
|
|
11
|
+
conditions:
|
|
12
|
+
|
|
13
|
+
2. The licensee may use the Software only in its original,
|
|
14
|
+
unmodified form.
|
|
15
|
+
3. All copies or substantial portions of the Software must
|
|
16
|
+
remain unaltered and include this copyright notice and these terms of use.
|
|
17
|
+
4. Use of the Software for projects not related to GN or
|
|
18
|
+
GW systems is strictly prohibited.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
21
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
22
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
|
|
23
|
+
A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT
|
|
24
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
25
|
+
CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
26
|
+
OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
27
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
28
|
+
DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024 KeyisB. All rights reserved.
|
|
3
|
+
|
|
4
|
+
Permission is hereby granted, free of charge, to any person
|
|
5
|
+
obtaining a copy of this software and associated documentation
|
|
6
|
+
files (the "Software"), to use the Software exclusively for
|
|
7
|
+
projects related to the GN or GW systems, including personal,
|
|
8
|
+
educational, and commercial purposes, subject to the following
|
|
9
|
+
conditions:
|
|
10
|
+
|
|
11
|
+
1. Copying, modification, merging, publishing, distribution,
|
|
12
|
+
sublicensing, and/or selling copies of the Software are
|
|
13
|
+
strictly prohibited.
|
|
14
|
+
2. The licensee may use the Software only in its original,
|
|
15
|
+
unmodified form.
|
|
16
|
+
3. All copies or substantial portions of the Software must
|
|
17
|
+
remain unaltered and include this copyright notice and these terms of use.
|
|
18
|
+
4. Use of the Software for projects not related to GN or
|
|
19
|
+
GW systems is strictly prohibited.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
|
|
22
|
+
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
|
23
|
+
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR
|
|
24
|
+
A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. IN NO EVENT
|
|
25
|
+
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
26
|
+
CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
27
|
+
OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR
|
|
28
|
+
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
29
|
+
DEALINGS IN THE SOFTWARE.
|
|
30
|
+
"""
|
|
31
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
|
|
2
|
+
def guess_type(filename: str) -> str:
|
|
3
|
+
"""
|
|
4
|
+
Возвращает актуальный MIME-тип по расширению файла.
|
|
5
|
+
Только современные и часто используемые типы.
|
|
6
|
+
"""
|
|
7
|
+
ext = filename.lower().rsplit('.', 1)[-1] if '.' in filename else ''
|
|
8
|
+
|
|
9
|
+
mime_map = {
|
|
10
|
+
# 🔹 Текст и данные
|
|
11
|
+
"txt": "text/plain",
|
|
12
|
+
"html": "text/html",
|
|
13
|
+
"css": "text/css",
|
|
14
|
+
"csv": "text/csv",
|
|
15
|
+
"xml": "application/xml",
|
|
16
|
+
"json": "application/json",
|
|
17
|
+
"js": "application/javascript",
|
|
18
|
+
|
|
19
|
+
# 🔹 Изображения (актуальные для веба)
|
|
20
|
+
"jpg": "image/jpeg",
|
|
21
|
+
"jpeg": "image/jpeg",
|
|
22
|
+
"png": "image/png",
|
|
23
|
+
"gif": "image/gif",
|
|
24
|
+
"webp": "image/webp",
|
|
25
|
+
"svg": "image/svg+xml",
|
|
26
|
+
"avif": "image/avif",
|
|
27
|
+
|
|
28
|
+
# 🔹 Видео (современные форматы)
|
|
29
|
+
"mp4": "video/mp4",
|
|
30
|
+
"webm": "video/webm",
|
|
31
|
+
|
|
32
|
+
# 🔹 Аудио (современные форматы)
|
|
33
|
+
"mp3": "audio/mpeg",
|
|
34
|
+
"ogg": "audio/ogg",
|
|
35
|
+
"oga": "audio/ogg",
|
|
36
|
+
"m4a": "audio/mp4",
|
|
37
|
+
"flac": "audio/flac",
|
|
38
|
+
|
|
39
|
+
# 🔹 Архивы
|
|
40
|
+
"zip": "application/zip",
|
|
41
|
+
"gz": "application/gzip",
|
|
42
|
+
"tar": "application/x-tar",
|
|
43
|
+
"7z": "application/x-7z-compressed",
|
|
44
|
+
"rar": "application/vnd.rar",
|
|
45
|
+
|
|
46
|
+
# 🔹 Документы (актуальные офисные)
|
|
47
|
+
"pdf": "application/pdf",
|
|
48
|
+
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
49
|
+
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
50
|
+
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
51
|
+
|
|
52
|
+
# 🔹 Шрифты
|
|
53
|
+
"woff": "font/woff",
|
|
54
|
+
"woff2": "font/woff2",
|
|
55
|
+
"ttf": "font/ttf",
|
|
56
|
+
"otf": "font/otf",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return mime_map.get(ext, "application/octet-stream")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
import re
|
|
63
|
+
from typing import List
|
|
64
|
+
|
|
65
|
+
# regex для !{var}, поддерживает вложенность через точку
|
|
66
|
+
TPL_VAR_RE = re.compile(r'(?<!\\)!\{([A-Za-z_][A-Za-z0-9_\.]*)\}')
|
|
67
|
+
|
|
68
|
+
# список mime, которые считаем текстовыми
|
|
69
|
+
TEXTUAL_MIME_PREFIXES = [
|
|
70
|
+
"text/", # text/html, text/css, text/plain
|
|
71
|
+
]
|
|
72
|
+
TEXTUAL_MIME_EXACT = {
|
|
73
|
+
"application/javascript",
|
|
74
|
+
"application/json",
|
|
75
|
+
"application/xml",
|
|
76
|
+
"application/xhtml+xml"
|
|
77
|
+
}
|
|
78
|
+
TEXTUAL_MIME_SUFFIXES = (
|
|
79
|
+
"+xml", # например application/rss+xml
|
|
80
|
+
"+json", # application/ld+json
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def extract_template_vars(filedata: bytes, mime: str) -> List[str]:
|
|
84
|
+
"""
|
|
85
|
+
Ищет все !{var} в тексте, если MIME относится к текстовым.
|
|
86
|
+
"""
|
|
87
|
+
mime = (mime or "").lower().strip()
|
|
88
|
+
|
|
89
|
+
# определяем, текстовый ли mime
|
|
90
|
+
is_textual = (
|
|
91
|
+
mime.startswith(tuple(TEXTUAL_MIME_PREFIXES))
|
|
92
|
+
or mime in TEXTUAL_MIME_EXACT
|
|
93
|
+
or mime.endswith(TEXTUAL_MIME_SUFFIXES)
|
|
94
|
+
or "javascript" in mime
|
|
95
|
+
or "json" in mime
|
|
96
|
+
or "xml" in mime
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
if not is_textual:
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
text = filedata.decode("utf-8", errors="ignore")
|
|
104
|
+
except Exception:
|
|
105
|
+
return []
|
|
106
|
+
|
|
107
|
+
return list(set(m.group(1) for m in TPL_VAR_RE.finditer(text)))
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
import re
|
|
111
|
+
import asyncio
|
|
112
|
+
from functools import lru_cache
|
|
113
|
+
from typing import Dict, Any
|
|
114
|
+
|
|
115
|
+
_SIGILS = (b'%', b'!', b'&')
|
|
116
|
+
_PATTERNS: dict[bytes, re.Pattern[bytes]] = {
|
|
117
|
+
s: re.compile(rb'(?<!\\)' + re.escape(s) + rb'\{([A-Za-z_][A-Za-z0-9_\.]*)\}')
|
|
118
|
+
for s in _SIGILS
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@lru_cache(maxsize=4096)
|
|
122
|
+
def _split_path(path: str) -> tuple[str, ...]:
|
|
123
|
+
return tuple(path.split('.'))
|
|
124
|
+
|
|
125
|
+
def _resolve(path: str, ctx: Dict[str, Any]) -> Any:
|
|
126
|
+
cur: Any = ctx
|
|
127
|
+
for k in _split_path(path):
|
|
128
|
+
if not isinstance(cur, dict) or k not in cur:
|
|
129
|
+
raise KeyError(path)
|
|
130
|
+
cur = cur[k]
|
|
131
|
+
return cur
|
|
132
|
+
|
|
133
|
+
def make_renderer(sigil: bytes = b'%'):
|
|
134
|
+
"""
|
|
135
|
+
Возвращает рендер для одного сигила. Работает с bytes.
|
|
136
|
+
"""
|
|
137
|
+
if sigil not in _PATTERNS:
|
|
138
|
+
raise ValueError(f"unsupported sigil: {sigil!r}")
|
|
139
|
+
rx = _PATTERNS[sigil]
|
|
140
|
+
esc_seq = b'\\' + sigil + b'{'
|
|
141
|
+
unesc_seq = sigil + b'{'
|
|
142
|
+
|
|
143
|
+
def render(data: bytes, ctx: Dict[str, Any], *, keep_unresolved: bool = False) -> bytes:
|
|
144
|
+
parts: list[bytes] = []
|
|
145
|
+
append = parts.append
|
|
146
|
+
last = 0
|
|
147
|
+
finditer = rx.finditer
|
|
148
|
+
|
|
149
|
+
for m in finditer(data):
|
|
150
|
+
start, end = m.span()
|
|
151
|
+
append(data[last:start])
|
|
152
|
+
|
|
153
|
+
key = m.group(1).decode('utf-8')
|
|
154
|
+
try:
|
|
155
|
+
val = _resolve(key, ctx)
|
|
156
|
+
append(b"" if val is None else str(val).encode('utf-8'))
|
|
157
|
+
except KeyError:
|
|
158
|
+
if keep_unresolved:
|
|
159
|
+
append(data[start:end])
|
|
160
|
+
else:
|
|
161
|
+
raise
|
|
162
|
+
last = end
|
|
163
|
+
|
|
164
|
+
if last < len(data):
|
|
165
|
+
append(data[last:])
|
|
166
|
+
|
|
167
|
+
out = b''.join(parts)
|
|
168
|
+
if esc_seq in out:
|
|
169
|
+
out = out.replace(esc_seq, unesc_seq)
|
|
170
|
+
return out
|
|
171
|
+
|
|
172
|
+
return render
|
|
173
|
+
|
|
174
|
+
# предсобранные
|
|
175
|
+
render_pct = make_renderer(b'%')
|
|
176
|
+
render_bang = make_renderer(b'!')
|
|
177
|
+
render_amp = make_renderer(b'&')
|
|
178
|
+
|
|
179
|
+
# асинхронные обёртки
|
|
180
|
+
_BIG = 256 * 1024
|
|
181
|
+
|
|
182
|
+
async def render_pct_async(data: bytes, ctx: Dict[str, Any], *, keep_unresolved: bool = False) -> bytes:
|
|
183
|
+
return render_pct(data, ctx, keep_unresolved=keep_unresolved) if len(data) < _BIG \
|
|
184
|
+
else await asyncio.to_thread(render_pct, data, ctx, keep_unresolved=keep_unresolved)
|
|
185
|
+
|
|
186
|
+
async def render_bang_async(data: bytes, ctx: Dict[str, Any], *, keep_unresolved: bool = False) -> bytes:
|
|
187
|
+
return render_bang(data, ctx, keep_unresolved=keep_unresolved) if len(data) < _BIG \
|
|
188
|
+
else await asyncio.to_thread(render_bang, data, ctx, keep_unresolved=keep_unresolved)
|
|
189
|
+
|
|
190
|
+
async def render_amp_async(data: bytes, ctx: Dict[str, Any], *, keep_unresolved: bool = False) -> bytes:
|
|
191
|
+
return render_amp(data, ctx, keep_unresolved=keep_unresolved) if len(data) < _BIG \
|
|
192
|
+
else await asyncio.to_thread(render_amp, data, ctx, keep_unresolved=keep_unresolved)
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Iterable, Optional, List, NamedTuple
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
|
|
5
|
+
_VERSION_RE = re.compile(r"^\d+(?:\.\d+)*(?:-\d+(?:\.\d+)*)?$").match
|
|
6
|
+
_is_ver = _VERSION_RE
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _to_list(v: str) -> List[int]:
|
|
10
|
+
return [int(x) for x in v.split(".")] if v else []
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _cmp(a: List[int], b: List[int]) -> int:
|
|
14
|
+
n = max(len(a), len(b))
|
|
15
|
+
a += [0] * (n - len(a))
|
|
16
|
+
b += [0] * (n - len(b))
|
|
17
|
+
return (a > b) - (a < b)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _VersionRange:
|
|
21
|
+
"""Одиночная версия, диапазон a‑b, 'last' или wildcard (None)."""
|
|
22
|
+
|
|
23
|
+
__slots__ = ("raw", "kind", "lo", "hi", "single")
|
|
24
|
+
|
|
25
|
+
def __init__(self, raw: Optional[str]):
|
|
26
|
+
self.raw = raw # None == wildcard
|
|
27
|
+
if raw is None:
|
|
28
|
+
self.kind = "wild"
|
|
29
|
+
return
|
|
30
|
+
if raw.lower() == "last":
|
|
31
|
+
self.kind = "single_last"
|
|
32
|
+
return
|
|
33
|
+
if "-" in raw:
|
|
34
|
+
self.kind = "range"
|
|
35
|
+
lo, hi = raw.split("-", 1)
|
|
36
|
+
self.lo = _to_list(lo)
|
|
37
|
+
self.hi = _to_list(hi)
|
|
38
|
+
else:
|
|
39
|
+
self.kind = "single"
|
|
40
|
+
self.single = _to_list(raw)
|
|
41
|
+
|
|
42
|
+
def contains(self, ver: Optional[str]) -> bool: # noqa: C901
|
|
43
|
+
if self.kind == "wild":
|
|
44
|
+
return True
|
|
45
|
+
ver = ver or "last"
|
|
46
|
+
if self.kind == "single_last":
|
|
47
|
+
return ver.lower() == "last"
|
|
48
|
+
if ver.lower() == "last":
|
|
49
|
+
return False
|
|
50
|
+
v = _to_list(ver)
|
|
51
|
+
if self.kind == "single":
|
|
52
|
+
return _cmp(self.single[:], v) == 0
|
|
53
|
+
return _cmp(self.lo[:], v) <= 0 <= _cmp(v, self.hi[:])
|
|
54
|
+
|
|
55
|
+
# for debugging / logs
|
|
56
|
+
def __str__(self) -> str:
|
|
57
|
+
return self.raw or "last"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class _Pat(NamedTuple):
|
|
61
|
+
gn_ver: _VersionRange
|
|
62
|
+
p1_name: Optional[str]
|
|
63
|
+
p1_ver: _VersionRange
|
|
64
|
+
p1_need_last: bool
|
|
65
|
+
p2_name: Optional[str]
|
|
66
|
+
p2_ver: _VersionRange
|
|
67
|
+
p2_need_last: bool
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@lru_cache(maxsize=2048)
|
|
71
|
+
def _compile_full_pattern(pat: str) -> _Pat:
|
|
72
|
+
t = pat.split(":")
|
|
73
|
+
gn_ver = _VersionRange(None)
|
|
74
|
+
if t and t[0].lower() == "gn":
|
|
75
|
+
t.pop(0)
|
|
76
|
+
gn_ver = _VersionRange(t.pop(0)) if t and (_is_ver(t[0]) or t[0].lower() == "last") else _VersionRange(None)
|
|
77
|
+
|
|
78
|
+
p2_name = p2_ver = p1_name = p1_ver = None
|
|
79
|
+
p2_need_last = p1_need_last = False
|
|
80
|
+
|
|
81
|
+
if t:
|
|
82
|
+
if _is_ver(t[-1]) or t[-1].lower() == "last":
|
|
83
|
+
p2_ver = _VersionRange(t.pop())
|
|
84
|
+
else:
|
|
85
|
+
p2_need_last = True
|
|
86
|
+
p2_name = t.pop() if t else None
|
|
87
|
+
|
|
88
|
+
if t:
|
|
89
|
+
if _is_ver(t[-1]) or t[-1].lower() == "last":
|
|
90
|
+
p1_ver = _VersionRange(t.pop())
|
|
91
|
+
else:
|
|
92
|
+
p1_need_last = True
|
|
93
|
+
p1_name = t.pop() if t else None
|
|
94
|
+
|
|
95
|
+
if t:
|
|
96
|
+
raise ValueError(f"bad pattern {pat!r}")
|
|
97
|
+
|
|
98
|
+
return _Pat(
|
|
99
|
+
gn_ver=gn_ver,
|
|
100
|
+
p1_name=None if p1_name is None else p1_name.lower(),
|
|
101
|
+
p1_ver=p1_ver or _VersionRange(None),
|
|
102
|
+
p1_need_last=p1_need_last,
|
|
103
|
+
p2_name=None if p2_name is None else p2_name.lower(),
|
|
104
|
+
p2_ver=p2_ver or _VersionRange(None),
|
|
105
|
+
p2_need_last=p2_need_last,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _LeafPat(NamedTuple):
|
|
110
|
+
name: Optional[str]
|
|
111
|
+
ver: _VersionRange
|
|
112
|
+
need_last: bool
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@lru_cache(maxsize=4096)
|
|
116
|
+
def _compile_leaf_pattern(pat: str) -> _LeafPat:
|
|
117
|
+
"""
|
|
118
|
+
pattern ::= NAME
|
|
119
|
+
| NAME ':' VERSION
|
|
120
|
+
| VERSION (# имя опущено)
|
|
121
|
+
"""
|
|
122
|
+
if ":" not in pat:
|
|
123
|
+
if _is_ver(pat) or pat.lower() == "last":
|
|
124
|
+
return _LeafPat(name=None, ver=_VersionRange(pat), need_last=False)
|
|
125
|
+
return _LeafPat(name=pat.lower(), ver=_VersionRange(None), need_last=True)
|
|
126
|
+
|
|
127
|
+
name, ver = pat.split(":", 1)
|
|
128
|
+
name = name.lower() or None
|
|
129
|
+
need_last = False
|
|
130
|
+
if not ver:
|
|
131
|
+
need_last = True
|
|
132
|
+
ver_range = _VersionRange(None)
|
|
133
|
+
else:
|
|
134
|
+
ver_range = _VersionRange(ver)
|
|
135
|
+
return _LeafPat(name=name, ver=ver_range, need_last=need_last)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class GNProtocol:
|
|
139
|
+
"""
|
|
140
|
+
Строка формата gn[:gnVer]:transport[:ver1]:route[:ver2]
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
__slots__ = (
|
|
144
|
+
"raw",
|
|
145
|
+
"gn_ver_raw",
|
|
146
|
+
"gn_ver",
|
|
147
|
+
"trnsp_name",
|
|
148
|
+
"trnsp_ver_raw",
|
|
149
|
+
"trnsp_ver",
|
|
150
|
+
"route_name",
|
|
151
|
+
"route_ver_raw",
|
|
152
|
+
"route_ver",
|
|
153
|
+
"_gn_leaf",
|
|
154
|
+
"_trnsp_leaf",
|
|
155
|
+
"_route_leaf",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# ---------------------------------------------------------------- init ---
|
|
159
|
+
def __init__(self, raw: str):
|
|
160
|
+
self.raw = raw
|
|
161
|
+
self._parse()
|
|
162
|
+
self._gn_leaf = self._LeafProto("gn", self.gn_ver_raw)
|
|
163
|
+
self._trnsp_leaf = self._LeafProto(self.trnsp_name, self.trnsp_ver_raw)
|
|
164
|
+
self._route_leaf = self._LeafProto(self.route_name, self.route_ver_raw)
|
|
165
|
+
|
|
166
|
+
# ---------------------------------------------------------------- parse --
|
|
167
|
+
@staticmethod
|
|
168
|
+
def _take_ver(tokens: List[str]) -> Optional[str]:
|
|
169
|
+
return tokens.pop(0) if tokens and (_is_ver(tokens[0]) or tokens[0].lower() == "last") else None
|
|
170
|
+
|
|
171
|
+
def _parse(self) -> None:
|
|
172
|
+
t = self.raw.split(":")
|
|
173
|
+
if not t or t[0].lower() != "gn":
|
|
174
|
+
raise ValueError("must start with 'gn'")
|
|
175
|
+
t.pop(0)
|
|
176
|
+
|
|
177
|
+
self.gn_ver_raw = self._take_ver(t)
|
|
178
|
+
self.gn_ver = _VersionRange(self.gn_ver_raw)
|
|
179
|
+
|
|
180
|
+
if not t:
|
|
181
|
+
raise ValueError("missing transport proto")
|
|
182
|
+
self.trnsp_name = t.pop(0).lower()
|
|
183
|
+
self.trnsp_ver_raw = self._take_ver(t)
|
|
184
|
+
self.trnsp_ver = _VersionRange(self.trnsp_ver_raw)
|
|
185
|
+
|
|
186
|
+
if not t:
|
|
187
|
+
raise ValueError("missing route proto")
|
|
188
|
+
self.route_name = t.pop(0).lower()
|
|
189
|
+
self.route_ver_raw = self._take_ver(t)
|
|
190
|
+
self.route_ver = _VersionRange(self.route_ver_raw)
|
|
191
|
+
|
|
192
|
+
if t:
|
|
193
|
+
raise ValueError(f"extra tokens: {t!r}")
|
|
194
|
+
|
|
195
|
+
def structure(self) -> dict:
|
|
196
|
+
return {
|
|
197
|
+
"gn": {"version": str(self.gn_ver)},
|
|
198
|
+
self.trnsp_name: {"version": str(self.trnsp_ver)},
|
|
199
|
+
self.route_name: {"version": str(self.route_ver)},
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
def matches_any(self, patterns: Iterable[str]) -> bool:
|
|
203
|
+
gv = self.gn_ver_raw
|
|
204
|
+
c_name, c_ver = self.trnsp_name, self.trnsp_ver_raw
|
|
205
|
+
r_name, r_ver = self.route_name, self.route_ver_raw
|
|
206
|
+
|
|
207
|
+
for pat in patterns:
|
|
208
|
+
gn_v, p1n, p1v, p1need, p2n, p2v, p2need = _compile_full_pattern(pat)
|
|
209
|
+
|
|
210
|
+
# gn
|
|
211
|
+
if not gn_v.contains(gv):
|
|
212
|
+
continue
|
|
213
|
+
|
|
214
|
+
# transport
|
|
215
|
+
if p1n and p1n != c_name:
|
|
216
|
+
continue
|
|
217
|
+
if p1need:
|
|
218
|
+
if c_ver is not None:
|
|
219
|
+
continue
|
|
220
|
+
elif not p1v.contains(c_ver):
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
# route
|
|
224
|
+
if p2n and p2n != r_name:
|
|
225
|
+
continue
|
|
226
|
+
if p2need:
|
|
227
|
+
if r_ver is not None:
|
|
228
|
+
continue
|
|
229
|
+
elif not p2v.contains(r_ver):
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
return True
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
class _LeafProto:
|
|
236
|
+
__slots__ = ("_name", "_ver_raw")
|
|
237
|
+
|
|
238
|
+
def __init__(self, name: str, ver_raw: Optional[str]):
|
|
239
|
+
self._name = name
|
|
240
|
+
self._ver_raw = ver_raw # None == 'last'
|
|
241
|
+
|
|
242
|
+
def protocol(self) -> str:
|
|
243
|
+
return self._name
|
|
244
|
+
|
|
245
|
+
def version(self) -> str:
|
|
246
|
+
return self._ver_raw or "last"
|
|
247
|
+
|
|
248
|
+
def matches_any(self, *patterns) -> bool:
|
|
249
|
+
if len(patterns) == 1 and not isinstance(patterns[0], str):
|
|
250
|
+
patterns_iter = patterns[0]
|
|
251
|
+
else:
|
|
252
|
+
patterns_iter = patterns
|
|
253
|
+
|
|
254
|
+
nm = self._name
|
|
255
|
+
vr = self._ver_raw
|
|
256
|
+
|
|
257
|
+
for p in patterns_iter:
|
|
258
|
+
pat = _compile_leaf_pattern(p)
|
|
259
|
+
|
|
260
|
+
if pat.name is not None and pat.name != nm:
|
|
261
|
+
continue
|
|
262
|
+
|
|
263
|
+
if pat.need_last:
|
|
264
|
+
if vr is not None:
|
|
265
|
+
continue
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
if pat.ver.contains(vr):
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
def __repr__(self) -> str:
|
|
274
|
+
return f"<Proto {self._name}:{self.version()}>"
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def gn(self) -> _LeafProto:
|
|
278
|
+
"""Top‑level 'gn' protocol."""
|
|
279
|
+
return self._gn_leaf
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def transport(self) -> _LeafProto:
|
|
283
|
+
return self._trnsp_leaf
|
|
284
|
+
|
|
285
|
+
@property
|
|
286
|
+
def route(self) -> _LeafProto:
|
|
287
|
+
return self._route_leaf
|
|
288
|
+
|
|
289
|
+
def __repr__(self) -> str:
|
|
290
|
+
return (
|
|
291
|
+
f"<GNProtocol gn:{self.gn_ver_raw or 'last'} "
|
|
292
|
+
f"{self.trnsp_name}:{self.trnsp_ver_raw or 'last'} "
|
|
293
|
+
f"{self.route_name}:{self.route_ver_raw or 'last'}>"
|
|
294
|
+
)
|