gnobjects 0.0.0.0.1__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.
- gnobjects/__init__.py +31 -0
- gnobjects/net/_func.py +192 -0
- gnobjects/net/_transport_pr_obj.py +294 -0
- gnobjects/net/fastcommands.py +165 -0
- gnobjects/net/objects.py +955 -0
- gnobjects-0.0.0.0.1.dist-info/METADATA +28 -0
- gnobjects-0.0.0.0.1.dist-info/RECORD +10 -0
- gnobjects-0.0.0.0.1.dist-info/WHEEL +5 -0
- gnobjects-0.0.0.0.1.dist-info/licenses/LICENSE +21 -0
- gnobjects-0.0.0.0.1.dist-info/top_level.txt +1 -0
gnobjects/__init__.py
ADDED
|
@@ -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
|
+
|
gnobjects/net/_func.py
ADDED
|
@@ -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
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from typing import Optional, Union, List
|
|
2
|
+
|
|
3
|
+
from KeyisBTools.models.serialization import SerializableType
|
|
4
|
+
|
|
5
|
+
from .objects import GNResponse, FileObject, CORSObject
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GNFastCommand(GNResponse):
|
|
10
|
+
"""
|
|
11
|
+
# Быстрый ответ
|
|
12
|
+
"""
|
|
13
|
+
def __init__(self,
|
|
14
|
+
payload: Optional[SerializableType] = None,
|
|
15
|
+
files: Optional[Union[str, FileObject, List[FileObject]]] = None,
|
|
16
|
+
cors: Optional[CORSObject] = None):
|
|
17
|
+
|
|
18
|
+
command = getattr(self, "cls_command", None)
|
|
19
|
+
if command is None:
|
|
20
|
+
command = 'gn:code-error:undefined'
|
|
21
|
+
|
|
22
|
+
super().__init__(command=command, payload=payload, files=files, cors=cors)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AllGNFastCommands:
|
|
26
|
+
class ok(GNFastCommand):
|
|
27
|
+
"""
|
|
28
|
+
# Корректный ответ
|
|
29
|
+
"""
|
|
30
|
+
cls_command = True
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class UnprocessableEntity(GNFastCommand):
|
|
34
|
+
"""
|
|
35
|
+
# Некорректные данные
|
|
36
|
+
Ошибка указывает, что сервер понял запрос, но не может его обработать из-за неверного содержания.
|
|
37
|
+
Пример: передан payload с правильной структурой, но поля содержат некорректные значения (например, строка вместо числа).
|
|
38
|
+
Используется, когда данные формально корректны, но нарушают бизнес-логику.
|
|
39
|
+
"""
|
|
40
|
+
cls_command = "gn:origin:422"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class BadRequest(GNFastCommand):
|
|
44
|
+
"""
|
|
45
|
+
# Неправильный синтаксис url или параметров
|
|
46
|
+
Сервер не может понять запрос из-за ошибок в структуре или параметрах.
|
|
47
|
+
Пример: отсутствует обязательный параметр или указан некорректный формат даты.
|
|
48
|
+
Часто используется при валидации входных данных на уровне запроса.
|
|
49
|
+
"""
|
|
50
|
+
cls_command = "gn:origin:400"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Forbidden(GNFastCommand):
|
|
54
|
+
"""
|
|
55
|
+
# Доступ запрещён, даже при наличии авторизации
|
|
56
|
+
Клиент аутентифицирован, но не имеет прав для выполнения действия.
|
|
57
|
+
Пример: пользователь вошёл в систему, но пытается изменить чужие данные.
|
|
58
|
+
Используется для разграничения прав доступа.
|
|
59
|
+
"""
|
|
60
|
+
cls_command = "gn:origin:403"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Unauthorized(GNFastCommand):
|
|
64
|
+
"""
|
|
65
|
+
# Требуется авторизация
|
|
66
|
+
Ошибка возвращается, если запрос требует входа, но клиент не предоставил или предоставил неверные данные авторизации.
|
|
67
|
+
Пример: отсутствует заголовок Authorization или токен недействителен.
|
|
68
|
+
Используется для защиты закрытых API-эндпоинтов.
|
|
69
|
+
"""
|
|
70
|
+
cls_command = "gn:origin:401"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class NotFound(GNFastCommand):
|
|
74
|
+
"""
|
|
75
|
+
# Ресурс не найден
|
|
76
|
+
Запрошенный объект или путь не существует на сервере.
|
|
77
|
+
Пример: попытка получить пользователя с несуществующим ID.
|
|
78
|
+
Часто используется для API-ответов на невалидные ссылки.
|
|
79
|
+
"""
|
|
80
|
+
cls_command = "gn:origin:404"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class MethodNotAllowed(GNFastCommand):
|
|
84
|
+
"""
|
|
85
|
+
# Метод запроса не поддерживается данным ресурсом
|
|
86
|
+
Ресурс существует, но используемый gn-метод недопустим.
|
|
87
|
+
Пример: к ресурсу разрешён только GET, а клиент делает POST.
|
|
88
|
+
Используется для ограничения набора действий над конкретными ресурсами.
|
|
89
|
+
"""
|
|
90
|
+
cls_command = "gn:origin:405"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Conflict(GNFastCommand):
|
|
94
|
+
"""
|
|
95
|
+
# Конфликт состояния ресурса (например, дубликат)
|
|
96
|
+
Возникает, когда операция не может быть выполнена из-за противоречия с текущим состоянием ресурса.
|
|
97
|
+
Пример: попытка зарегистрировать пользователя с уже существующим email.
|
|
98
|
+
Используется для предотвращения логических коллизий.
|
|
99
|
+
"""
|
|
100
|
+
cls_command = "gn:origin:409"
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class InternalServerError(GNFastCommand):
|
|
104
|
+
"""
|
|
105
|
+
# Внутренняя ошибка сервера
|
|
106
|
+
Сервер столкнулся с непредвиденной ситуацией, которая не позволяет выполнить запрос.
|
|
107
|
+
Пример: необработанное исключение в коде приложения.
|
|
108
|
+
Используется как универсальная ошибка для внутренних сбоев.
|
|
109
|
+
"""
|
|
110
|
+
cls_command = "gn:origin:500"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class NotImplemented(GNFastCommand):
|
|
114
|
+
"""
|
|
115
|
+
# Метод или функционал ещё не реализован
|
|
116
|
+
Сервер распознаёт запрос, но не поддерживает требуемый функционал.
|
|
117
|
+
Пример: метод API описан в документации, но ещё не реализован.
|
|
118
|
+
Используется для обозначения незавершённых частей системы.
|
|
119
|
+
"""
|
|
120
|
+
cls_command = "gn:origin:501"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class BadGateway(GNFastCommand):
|
|
124
|
+
"""
|
|
125
|
+
# Ошибка шлюза или прокси при обращении к апстриму
|
|
126
|
+
Промежуточный сервер получил некорректный ответ от апстрим-сервера.
|
|
127
|
+
Пример: API-шлюз обращается к backend, но тот вернул ошибку.
|
|
128
|
+
"""
|
|
129
|
+
cls_command = "gn:origin:502"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class ServiceUnavailable(GNFastCommand):
|
|
133
|
+
"""
|
|
134
|
+
# Сервис временно недоступен
|
|
135
|
+
Сервер не может обработать запрос из-за перегрузки или обслуживания.
|
|
136
|
+
Пример: база данных недоступна или сервис в режиме обновления.
|
|
137
|
+
Используется для сигнализации о временных проблемах.
|
|
138
|
+
"""
|
|
139
|
+
cls_command = "gn:origin:503"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class GatewayTimeout(GNFastCommand):
|
|
143
|
+
"""
|
|
144
|
+
# Таймаут при обращении к апстриму
|
|
145
|
+
Прокси или шлюз не дождался ответа от вышестоящего сервера в установленный срок.
|
|
146
|
+
Пример: запрос к медленному backend-сервису превысил лимит времени.
|
|
147
|
+
Используется для контроля SLA и таймаутов в распределённых системах.
|
|
148
|
+
"""
|
|
149
|
+
cls_command = "gn:origin:504"
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
globals().update({
|
|
155
|
+
name: obj
|
|
156
|
+
for name, obj in AllGNFastCommands.__dict__.items()
|
|
157
|
+
if isinstance(obj, type) and issubclass(obj, GNFastCommand)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|