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 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
+