GNServer 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.
@@ -0,0 +1,10 @@
1
+
2
+
3
+ from ._app import App
4
+
5
+
6
+
7
+
8
+
9
+
10
+
@@ -0,0 +1,400 @@
1
+
2
+
3
+
4
+ import re
5
+ import uuid
6
+ import anyio
7
+ import decimal
8
+ import asyncio
9
+ import inspect
10
+ import traceback
11
+ import datetime
12
+ import KeyisBClient
13
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Pattern, Tuple, Union, AsyncGenerator
14
+ from dataclasses import dataclass
15
+ from aioquic.asyncio.server import serve
16
+ from aioquic.asyncio.protocol import QuicConnectionProtocol
17
+ from aioquic.quic.configuration import QuicConfiguration
18
+ from aioquic.quic.events import QuicEvent, StreamDataReceived
19
+ from typing import Any, AsyncGenerator, Union, get_origin, get_args
20
+ from urllib.parse import parse_qs
21
+
22
+ from KeyisBClient import gn
23
+
24
+ import sys
25
+
26
+ try:
27
+ if not sys.platform.startswith("win"):
28
+ import uvloop
29
+ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
30
+ except ImportError:
31
+ print("uvloop не установлен")
32
+
33
+
34
+
35
+
36
+
37
+ import logging
38
+
39
+ logger = logging.getLogger("GNServer")
40
+ logger.setLevel(logging.DEBUG)
41
+
42
+ console = logging.StreamHandler()
43
+ console.setLevel(logging.INFO)
44
+ console.setFormatter(logging.Formatter("[GNServer] %(name)s: %(levelname)s: %(message)s"))
45
+
46
+
47
+
48
+ @dataclass
49
+ class Route:
50
+ method: str
51
+ path_expr: str
52
+ regex: Pattern[str]
53
+ param_types: dict[str, Callable[[str], Any]]
54
+ handler: Callable[..., Any]
55
+ name: str
56
+
57
+ _PARAM_REGEX: dict[str, str] = {
58
+ "str": r"[^/]+",
59
+ "path": r".+",
60
+ "int": r"\d+",
61
+ "float": r"[+-]?\d+(?:\.\d+)?",
62
+ "bool": r"(?:true|false|1|0)",
63
+ "uuid": r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-"
64
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
65
+ r"[0-9a-fA-F]{12}",
66
+ "datetime": r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?",
67
+ "date": r"\d{4}-\d{2}-\d{2}",
68
+ "time": r"\d{2}:\d{2}:\d{2}(?:\.\d+)?",
69
+ "decimal": r"[+-]?\d+(?:\.\d+)?",
70
+ }
71
+
72
+ _CONVERTER_FUNC: dict[str, Callable[[str], Any]] = {
73
+ "int": int,
74
+ "float": float,
75
+ "bool": lambda s: s.lower() in {"1","true","yes","on"},
76
+ "uuid": uuid.UUID,
77
+ "decimal": decimal.Decimal,
78
+ "datetime": datetime.datetime.fromisoformat,
79
+ "date": datetime.date.fromisoformat,
80
+ "time": datetime.time.fromisoformat,
81
+ }
82
+
83
+ def _compile_path(path: str) -> tuple[Pattern[str], dict[str, Callable[[str], Any]]]:
84
+ param_types: dict[str, Callable[[str], Any]] = {}
85
+ rx_parts: list[str] = ["^"]
86
+ i = 0
87
+ while i < len(path):
88
+ if path[i] != "{":
89
+ rx_parts.append(re.escape(path[i]))
90
+ i += 1
91
+ continue
92
+ j = path.index("}", i)
93
+ spec = path[i+1:j]
94
+ i = j + 1
95
+
96
+ if ":" in spec:
97
+ name, conv = spec.split(":", 1)
98
+ else:
99
+ name, conv = spec, "str"
100
+
101
+ if conv.startswith("^"):
102
+ rx = f"(?P<{name}>{conv})"
103
+ typ = str
104
+ else:
105
+ rx = f"(?P<{name}>{_PARAM_REGEX.get(conv, _PARAM_REGEX['str'])})"
106
+ typ = _CONVERTER_FUNC.get(conv, str)
107
+
108
+ rx_parts.append(rx)
109
+ param_types[name] = typ
110
+
111
+ rx_parts.append("$")
112
+ return re.compile("".join(rx_parts)), param_types
113
+
114
+ def _convert_value(raw: str | list[str], ann: Any, fallback: Callable[[str], Any]) -> Any:
115
+ origin = get_origin(ann)
116
+ args = get_args(ann)
117
+
118
+ if isinstance(raw, list) or origin is list:
119
+ subtype = args[0] if (origin is list and args) else str
120
+ if not isinstance(raw, list):
121
+ raw = [raw]
122
+ return [_convert_value(r, subtype, fallback) for r in raw]
123
+
124
+ conv = _CONVERTER_FUNC.get(ann, ann) if ann is not inspect._empty else fallback
125
+ return conv(raw) if callable(conv) else raw
126
+
127
+ def _ensure_async(fn: Callable[..., Any]) -> Callable[..., Any]:
128
+ if inspect.iscoroutinefunction(fn) or inspect.isasyncgenfunction(fn):
129
+ return fn
130
+ async def wrapper(*args, **kw):
131
+ return fn(*args, **kw)
132
+ return wrapper
133
+
134
+ class App:
135
+ def __init__(self):
136
+ self._routes: List[Route] = []
137
+
138
+ def route(self, method: str, path: str, *, name: str | None = None):
139
+ def decorator(fn: Callable[..., Any]):
140
+ regex, param_types = _compile_path(path)
141
+ self._routes.append(
142
+ Route(
143
+ method.upper(),
144
+ path,
145
+ regex,
146
+ param_types,
147
+ _ensure_async(fn),
148
+ name or fn.__name__,
149
+ )
150
+ )
151
+ return fn
152
+ return decorator
153
+
154
+ def get(self, path: str, *, name: str | None = None):
155
+ return self.route("GET", path, name=name)
156
+
157
+ def post(self, path: str, *, name: str | None = None):
158
+ return self.route("POST", path, name=name)
159
+
160
+ def put(self, path: str, *, name: str | None = None):
161
+ return self.route("PUT", path, name=name)
162
+
163
+ def delete(self, path: str, *, name: str | None = None):
164
+ return self.route("DELETE", path, name=name)
165
+
166
+ def custom(self, method: str, path: str, *, name: str | None = None):
167
+ return self.route(method, path, name=name)
168
+
169
+
170
+ async def dispatch(
171
+ self, request: gn.GNRequest
172
+ ) -> Union[gn.GNResponse, AsyncGenerator[gn.GNResponse, None]]:
173
+ path = request.url.path
174
+ method = request.method.upper()
175
+ cand = {path, path.rstrip("/") or "/", f"{path}/"}
176
+ allowed = set()
177
+
178
+ for r in self._routes:
179
+ m = next((r.regex.fullmatch(p) for p in cand if r.regex.fullmatch(p)), None)
180
+ if not m:
181
+ continue
182
+
183
+ allowed.add(r.method)
184
+ if r.method != method:
185
+ continue
186
+
187
+ sig = inspect.signature(r.handler)
188
+ def _ann(name: str):
189
+ param = sig.parameters.get(name)
190
+ return param.annotation if param else inspect._empty
191
+
192
+ kw: dict[str, Any] = {
193
+ name: _convert_value(val, _ann(name), r.param_types.get(name, str))
194
+ for name, val in m.groupdict().items()
195
+ }
196
+
197
+ for qn, qvals in parse_qs(request.url.query, keep_blank_values=True).items():
198
+ if qn in kw:
199
+ continue
200
+ raw = qvals if len(qvals) > 1 else qvals[0]
201
+ kw[qn] = _convert_value(raw, _ann(qn), str)
202
+
203
+ if "request" in sig.parameters:
204
+ kw["request"] = request
205
+
206
+ if inspect.isasyncgenfunction(r.handler):
207
+ return r.handler(**kw)
208
+
209
+ result = await r.handler(**kw)
210
+ if isinstance(result, gn.GNResponse):
211
+ return result
212
+ raise TypeError(
213
+ f"{r.handler.__name__} returned {type(result)}; GNResponse expected"
214
+ )
215
+
216
+ if allowed:
217
+ return gn.GNResponse("gn:origin:405", {'error': 'Method Not Allowed'})
218
+ return gn.GNResponse("gn:origin:404", {'error': 'Not Found'})
219
+
220
+
221
+ class _ServerProto(QuicConnectionProtocol):
222
+ def __init__(self, *a, api: "App", **kw):
223
+ super().__init__(*a, **kw)
224
+ self._api = api
225
+ self._buffer: Dict[int, bytearray] = {}
226
+ self._streams: Dict[int, Tuple[asyncio.Queue[Optional[gn.GNRequest]], bool]] = {}
227
+
228
+ def quic_event_received(self, event: QuicEvent):
229
+ if isinstance(event, StreamDataReceived):
230
+ buf = self._buffer.setdefault(event.stream_id, bytearray())
231
+ buf.extend(event.data)
232
+
233
+ # пока не знаем, это стрим или нет
234
+
235
+ if len(buf) < 8: # не дошел даже frame пакета
236
+ logger.debug(f'Пакет отклонен: {buf} < 8. Не доставлен фрейм')
237
+ return
238
+
239
+
240
+ # получаем длинну пакета
241
+ mode, stream, lenght = gn.GNRequest.type(buf)
242
+
243
+ if mode != 2: # не наш пакет
244
+ logger.debug(f'Пакет отклонен: mode пакета {mode}. Разрешен 2')
245
+ return
246
+
247
+ if not stream: # если не стрим, то ждем конец quic стрима и запускаем обработку ответа
248
+ if event.end_stream:
249
+ request = gn.GNRequest.deserialize(buf, 2)
250
+ # request.stream_id = event.stream_id
251
+ # loop = asyncio.get_event_loop()
252
+ # request.fut = loop.create_future()
253
+
254
+ request.stream_id = event.stream_id
255
+ asyncio.create_task(self._handle_request(request))
256
+ logger.debug(f'Отправлена задача разрешения пакета {request} route -> {request.route}')
257
+
258
+ self._buffer.pop(event.stream_id, None)
259
+ return
260
+
261
+ # если стрим, то смотрим сколько пришло данных
262
+ if len(buf) < lenght: # если пакет не весь пришел, пропускаем
263
+ return
264
+
265
+ # первый в буфере пакет пришел полностью
266
+
267
+ # берем пакет
268
+ data = buf[:lenght]
269
+
270
+ # удаляем его из буфера
271
+ del buf[:lenght]
272
+
273
+ # формируем запрос
274
+ request = gn.GNRequest.deserialize(data, 2)
275
+
276
+ logger.debug(request, f'event.stream_id -> {event.stream_id}')
277
+
278
+ request.stream_id = event.stream_id
279
+
280
+ queue, inapi = self._streams.setdefault(event.stream_id, (asyncio.Queue(), False))
281
+
282
+ if request.method == 'gn:end-stream':
283
+ if event.stream_id in self._streams:
284
+ _ = self._streams.get(event.stream_id)
285
+ if _ is not None:
286
+ queue, inapi = _
287
+ if inapi:
288
+ queue.put_nowait(None)
289
+ self._buffer.pop(event.stream_id)
290
+ self._streams.pop(event.stream_id)
291
+ logger.debug(f'Закрываем стрим [{event.stream_id}]')
292
+ return
293
+
294
+
295
+
296
+
297
+ queue.put_nowait(request)
298
+
299
+ # отдаем очередь в интерфейс
300
+ if not inapi:
301
+ self._streams[event.stream_id] = (queue, True)
302
+
303
+ async def w():
304
+ while True:
305
+ chunk = await queue.get()
306
+ if chunk is None:
307
+ break
308
+ yield chunk
309
+
310
+ request._stream = w
311
+ asyncio.create_task(self._handle_request(request))
312
+
313
+ async def _handle_request(self, request: gn.GNRequest):
314
+ try:
315
+
316
+ response = await self._api.dispatch(request)
317
+
318
+ response = await self.resolve_extra_response(response)
319
+
320
+
321
+ if inspect.isasyncgen(response):
322
+ async for chunk in response: # type: ignore[misc]
323
+ chunk._stream = True
324
+ self._quic.send_stream_data(request.stream_id, chunk.serialize(3), end_stream=False)
325
+ self.transmit()
326
+
327
+ l = gn.GNResponse('gn:end-stream')
328
+ l._stream = True
329
+ self._quic.send_stream_data(request.stream_id, l.serialize(3), end_stream=True)
330
+ self.transmit()
331
+ return
332
+
333
+
334
+ self._quic.send_stream_data(request.stream_id, response.serialize(3), end_stream=True)
335
+ logger.debug(f'Отправлен на сервер ответ -> {response.command()} {response.payload if len(response.payload) < 200 else ''}')
336
+ self.transmit()
337
+ except Exception as e:
338
+ logger.error('GNServer: error\n' + traceback.format_exc())
339
+
340
+ response = gn.GNResponse('gn:origin:500:Internal Server Error')
341
+ self._quic.send_stream_data(request.stream_id, response.serialize(3), end_stream=True)
342
+ self.transmit()
343
+
344
+ async def resolve_extra_response(self, response: Union[gn.GNResponse, AsyncGenerator[gn.GNResponse, None]]) -> Union[gn.GNResponse, AsyncGenerator[gn.GNResponse, None]]:
345
+
346
+ file_types = (
347
+ 'html',
348
+ 'css',
349
+ 'js',
350
+ 'svg'
351
+ )
352
+
353
+ if isinstance(response, gn.GNResponse):
354
+ payload = response.payload
355
+ if payload is not None:
356
+ for ext_file in file_types:
357
+ ext_file_ = payload.get(ext_file)
358
+ if ext_file_ is not None:
359
+ if isinstance(ext_file_, str):
360
+ if ext_file_.startswith('/') or ext_file_.startswith('./'):
361
+ try:
362
+ async with await anyio.open_file(ext_file_, mode="rb") as file:
363
+ payload[ext_file] = await file.read()
364
+ except Exception as e:
365
+ payload['html'] = f'GNServer error: {e}'
366
+ logger.debug(f'error resolving extra response -> {traceback.format_exc()}')
367
+
368
+
369
+
370
+ return response
371
+
372
+
373
+
374
+ def run(
375
+ self,
376
+ host: str,
377
+ port: int,
378
+ cert_path: str,
379
+ key_path: str,
380
+ *,
381
+ idle_timeout: float = 20.0,
382
+ wait: bool = True
383
+ ):
384
+ cfg = QuicConfiguration(
385
+ alpn_protocols=["gn:backend"], is_client=False, idle_timeout=idle_timeout
386
+ )
387
+ cfg.load_cert_chain(cert_path, key_path)
388
+
389
+ async def _main():
390
+ await serve(
391
+ host,
392
+ port,
393
+ configuration=cfg,
394
+ create_protocol=lambda *a, **kw: App._ServerProto(*a, api=self, **kw),
395
+ retry=False,
396
+ )
397
+ if wait:
398
+ await asyncio.Event().wait()
399
+
400
+ asyncio.run(_main())
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: GNServer
3
+ Version: 0.0.0.0.1
4
+ Summary: GNServer
5
+ Home-page: https://github.com/KeyisB/libs/tree/main/GNServer
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: aioquic
15
+ Requires-Dist: anyio
16
+ Requires-Dist: KeyisBClient
17
+ Requires-Dist: uvloop
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ GW and MMB Project libraries
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ setup.py
4
+ GNServer/LICENSE
5
+ GNServer/mmbConfig.json
6
+ GNServer/GNServer/__init__.py
7
+ GNServer/GNServer/_app.py
8
+ GNServer/GNServer.egg-info/PKG-INFO
9
+ GNServer/GNServer.egg-info/SOURCES.txt
10
+ GNServer/GNServer.egg-info/dependency_links.txt
11
+ GNServer/GNServer.egg-info/requires.txt
12
+ GNServer/GNServer.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ aioquic
2
+ anyio
3
+ KeyisBClient
4
+ uvloop
@@ -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 documentation
5
+ files (the "Software"), to use the Software exclusively for
6
+ projects related to the GN or GW systems, including personal,
7
+ educational, and commercial purposes, subject to the following
8
+ conditions:
9
+
10
+ 1. Copying, modification, merging, publishing, distribution,
11
+ sublicensing, and/or selling copies of the Software are
12
+ strictly prohibited.
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,8 @@
1
+ {
2
+ "requirements": [
3
+ "aioquic",
4
+ "anyio",
5
+ "KeyisBClient",
6
+ "uvloop"
7
+ ]
8
+ }
@@ -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,4 @@
1
+ graft GNServer
2
+ global-exclude __pycache__ *.py[cod] .DS_Store
3
+ include LICENSE
4
+ include README.md
@@ -0,0 +1,30 @@
1
+ Metadata-Version: 2.4
2
+ Name: GNServer
3
+ Version: 0.0.0.0.1
4
+ Summary: GNServer
5
+ Home-page: https://github.com/KeyisB/libs/tree/main/GNServer
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: aioquic
15
+ Requires-Dist: anyio
16
+ Requires-Dist: KeyisBClient
17
+ Requires-Dist: uvloop
18
+ Dynamic: author
19
+ Dynamic: author-email
20
+ Dynamic: classifier
21
+ Dynamic: description
22
+ Dynamic: description-content-type
23
+ Dynamic: home-page
24
+ Dynamic: license
25
+ Dynamic: license-file
26
+ Dynamic: requires-dist
27
+ Dynamic: requires-python
28
+ Dynamic: summary
29
+
30
+ GW and MMB Project libraries
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ from setuptools import setup
2
+
3
+ name = 'GNServer'
4
+ filesName = 'GNServer'
5
+
6
+ setup(
7
+ name=name,
8
+ version='0.0.0.0.1',
9
+ author="KeyisB",
10
+ author_email="keyisb.pip@gmail.com",
11
+ description=name,
12
+ long_description = 'GW and MMB Project libraries',
13
+ long_description_content_type= 'text/plain',
14
+ url=f"https://github.com/KeyisB/libs/tree/main/{name}",
15
+ include_package_data=True,
16
+ package_data = {}, # type: ignore
17
+ package_dir={'': f'{filesName}'.replace('-','_')},
18
+ classifiers=[
19
+ "Programming Language :: Python :: 3",
20
+ "Operating System :: OS Independent",
21
+ ],
22
+ python_requires='>=3.12',
23
+ license="MMB License v1.0",
24
+ install_requires = ['aioquic', 'anyio', 'KeyisBClient', 'uvloop'],
25
+ )