GNServer 0.0.0.0.20__tar.gz → 0.0.0.0.22__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.
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer/__init__.py +2 -1
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer/_app.py +85 -7
- gnserver-0.0.0.0.22/GNServer/GNServer/tools/__init__.py +1 -0
- gnserver-0.0.0.0.22/GNServer/GNServer/tools/_models.py +77 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer.egg-info/PKG-INFO +1 -1
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer.egg-info/SOURCES.txt +3 -1
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/PKG-INFO +1 -1
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/setup.py +1 -1
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer/___client.py +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer/_client.py +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer.egg-info/dependency_links.txt +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer.egg-info/requires.txt +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/GNServer.egg-info/top_level.txt +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/LICENSE +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/GNServer/mmbConfig.json +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/LICENSE +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/MANIFEST.in +0 -0
- {gnserver-0.0.0.0.20 → gnserver-0.0.0.0.22}/setup.cfg +0 -0
@@ -29,8 +29,9 @@ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
29
29
|
DEALINGS IN THE SOFTWARE.
|
30
30
|
"""
|
31
31
|
|
32
|
-
from ._app import App
|
32
|
+
from ._app import App, GNExceptions
|
33
33
|
from KeyisBClient.gn import GNRequest, GNResponse, CORSObject, FileObject, TemplateObject
|
34
|
+
from KeyisBClient import Url
|
34
35
|
|
35
36
|
|
36
37
|
|
@@ -52,19 +52,97 @@ class _BaseEXception(Exception):
|
|
52
52
|
self._message = message
|
53
53
|
|
54
54
|
def assembly(self):
|
55
|
+
"""
|
56
|
+
Собирает ошибку в ответ типа `GNResponse`
|
57
|
+
"""
|
55
58
|
return gn.GNResponse(f'gn:error:{self._code}', payload={'msg': self._message})
|
56
59
|
|
57
60
|
|
58
61
|
class GNExceptions:
|
59
62
|
class UnprocessableEntity(_BaseEXception):
|
60
63
|
def __init__(self):
|
64
|
+
"""
|
65
|
+
# Некорректные данные
|
66
|
+
"""
|
61
67
|
super().__init__('422', "Unprocessable Entity")
|
62
68
|
|
63
69
|
class BadRequest(_BaseEXception):
|
64
70
|
def __init__(self):
|
71
|
+
"""
|
72
|
+
# неправильного синтаксис url или параметров
|
73
|
+
"""
|
65
74
|
super().__init__('400', "Bad Request")
|
66
75
|
|
76
|
+
class Forbidden(_BaseEXception):
|
77
|
+
def __init__(self):
|
78
|
+
"""
|
79
|
+
# Доступ запрещён, даже при наличии авторизации
|
80
|
+
"""
|
81
|
+
super().__init__('403', "Forbidden")
|
82
|
+
|
83
|
+
|
84
|
+
class NotFound(_BaseEXception):
|
85
|
+
def __init__(self):
|
86
|
+
"""
|
87
|
+
# Ресурс не найден
|
88
|
+
"""
|
89
|
+
super().__init__('404', "Not Found")
|
90
|
+
|
91
|
+
|
92
|
+
class MethodNotAllowed(_BaseEXception):
|
93
|
+
def __init__(self):
|
94
|
+
"""
|
95
|
+
# Метод запроса не поддерживается данным ресурсом
|
96
|
+
"""
|
97
|
+
super().__init__('405', "Method Not Allowed")
|
98
|
+
|
99
|
+
|
100
|
+
class Conflict(_BaseEXception):
|
101
|
+
def __init__(self):
|
102
|
+
"""
|
103
|
+
# Конфликт состояния ресурса (например, дубликат)
|
104
|
+
"""
|
105
|
+
super().__init__('409', "Conflict")
|
106
|
+
|
107
|
+
|
108
|
+
class InternalServerError(_BaseEXception):
|
109
|
+
def __init__(self):
|
110
|
+
"""
|
111
|
+
# Внутренняя ошибка сервера
|
112
|
+
"""
|
113
|
+
super().__init__('500', "Internal Server Error")
|
114
|
+
|
115
|
+
|
116
|
+
class NotImplemented(_BaseEXception):
|
117
|
+
def __init__(self):
|
118
|
+
"""
|
119
|
+
# Метод или функционал ещё не реализован
|
120
|
+
"""
|
121
|
+
super().__init__('501', "Not Implemented")
|
122
|
+
|
123
|
+
|
124
|
+
class BadGateway(_BaseEXception):
|
125
|
+
def __init__(self):
|
126
|
+
"""
|
127
|
+
# Ошибка шлюза или прокси при обращении к апстриму
|
128
|
+
"""
|
129
|
+
super().__init__('502', "Bad Gateway")
|
130
|
+
|
131
|
+
|
132
|
+
class ServiceUnavailable(_BaseEXception):
|
133
|
+
def __init__(self):
|
134
|
+
"""
|
135
|
+
# Сервис временно недоступен
|
136
|
+
"""
|
137
|
+
super().__init__('503', "Service Unavailable")
|
138
|
+
|
67
139
|
|
140
|
+
class GatewayTimeout(_BaseEXception):
|
141
|
+
def __init__(self):
|
142
|
+
"""
|
143
|
+
# Таймаут при обращении к апстриму
|
144
|
+
"""
|
145
|
+
super().__init__('504', "Gateway Timeout")
|
68
146
|
|
69
147
|
def guess_type(filename: str) -> str:
|
70
148
|
"""
|
@@ -552,8 +630,8 @@ class App:
|
|
552
630
|
)
|
553
631
|
|
554
632
|
if allowed:
|
555
|
-
|
556
|
-
|
633
|
+
raise GNExceptions.MethodNotAllowed()
|
634
|
+
raise GNExceptions.NotFound()
|
557
635
|
|
558
636
|
|
559
637
|
def fastFile(self, path: str, file_path: str, cors: Optional[gn.CORSObject] = None, template: Optional[gn.TemplateObject] = None, payload: Optional[dict] = None):
|
@@ -564,7 +642,7 @@ class App:
|
|
564
642
|
file_path = file_path[:-1]
|
565
643
|
|
566
644
|
if not os.path.isfile(file_path):
|
567
|
-
|
645
|
+
raise GNExceptions.NotFound()
|
568
646
|
|
569
647
|
fileObject = gn.FileObject(file_path, template)
|
570
648
|
return gn.GNResponse('ok', payload=payload, files=fileObject, cors=cors)
|
@@ -579,7 +657,7 @@ class App:
|
|
579
657
|
file_path = file_path[:-1]
|
580
658
|
|
581
659
|
if not os.path.isfile(file_path):
|
582
|
-
|
660
|
+
raise GNExceptions.NotFound()
|
583
661
|
|
584
662
|
fileObject = gn.FileObject(file_path, template)
|
585
663
|
return gn.GNResponse('ok', payload=payload, files=fileObject, cors=cors)
|
@@ -591,8 +669,8 @@ class App:
|
|
591
669
|
@self.post('/!gn-vm-host/ping', cors=gn.CORSObject(None))
|
592
670
|
async def r_ping(request: gn.GNRequest):
|
593
671
|
|
594
|
-
if request.
|
595
|
-
|
672
|
+
if request.client.remote_addr != '127.0.0.1':
|
673
|
+
raise GNExceptions.Forbidden()
|
596
674
|
return gn.GNResponse('ok', {'time': datetime.datetime.now(datetime.UTC).isoformat()})
|
597
675
|
|
598
676
|
|
@@ -692,7 +770,7 @@ class App:
|
|
692
770
|
|
693
771
|
async def _handle_request(self, request: gn.GNRequest, mode: int):
|
694
772
|
|
695
|
-
request.
|
773
|
+
request.client._data['remote_addr'] = self._quic._network_paths[0].addr
|
696
774
|
|
697
775
|
try:
|
698
776
|
|
@@ -0,0 +1 @@
|
|
1
|
+
from ._models import TTLDict
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import time
|
2
|
+
import threading
|
3
|
+
|
4
|
+
class TTLDict:
|
5
|
+
def __init__(self, default_ttl: int = 60, cleanup_interval: int = 300):
|
6
|
+
"""
|
7
|
+
:param default_ttl: TTL по умолчанию (сек), если при записи не указан
|
8
|
+
:param cleanup_interval: периодическая очистка от просроченных ключей (сек), по умолчанию 5 мин
|
9
|
+
"""
|
10
|
+
self._store = {}
|
11
|
+
self._lock = threading.Lock()
|
12
|
+
self._default_ttl = default_ttl
|
13
|
+
self._cleanup_interval = cleanup_interval
|
14
|
+
self._stop_event = threading.Event()
|
15
|
+
|
16
|
+
self._timer = threading.Thread(target=self._cleanup_worker, daemon=True)
|
17
|
+
self._timer.start()
|
18
|
+
|
19
|
+
def set(self, key, value, ttl: int = None):
|
20
|
+
"""Установить значение с временем жизни (в секундах)."""
|
21
|
+
if ttl is None:
|
22
|
+
ttl = self._default_ttl
|
23
|
+
expire_at = time.monotonic() + ttl
|
24
|
+
with self._lock:
|
25
|
+
self._store[key] = (value, expire_at)
|
26
|
+
|
27
|
+
def get(self, key):
|
28
|
+
"""Получить значение или None, если ключ отсутствует/просрочен."""
|
29
|
+
now = time.monotonic()
|
30
|
+
with self._lock:
|
31
|
+
item = self._store.get(key)
|
32
|
+
if not item:
|
33
|
+
return None
|
34
|
+
|
35
|
+
value, expire_at = item
|
36
|
+
if expire_at < now:
|
37
|
+
del self._store[key]
|
38
|
+
return None
|
39
|
+
|
40
|
+
return value
|
41
|
+
|
42
|
+
def __setitem__(self, key, value):
|
43
|
+
"""
|
44
|
+
cache[key] = val → TTL по умолчанию
|
45
|
+
"""
|
46
|
+
self.set(key, value, self._default_ttl)
|
47
|
+
|
48
|
+
def __getitem__(self, key):
|
49
|
+
"""cache[key] -> value | None"""
|
50
|
+
return self.get(key)
|
51
|
+
|
52
|
+
def __contains__(self, key):
|
53
|
+
return self.get(key) is not None
|
54
|
+
|
55
|
+
def __len__(self):
|
56
|
+
return len(self._store)
|
57
|
+
|
58
|
+
def __repr__(self):
|
59
|
+
return f"<TTLDict size={len(self._store)}>"
|
60
|
+
|
61
|
+
def _cleanup_worker(self):
|
62
|
+
"""Фоновый поток для периодической очистки."""
|
63
|
+
while not self._stop_event.wait(self._cleanup_interval):
|
64
|
+
self.cleanup()
|
65
|
+
|
66
|
+
def cleanup(self):
|
67
|
+
"""Удалить все просроченные ключи."""
|
68
|
+
now = time.monotonic()
|
69
|
+
with self._lock:
|
70
|
+
expired = [k for k, (_, exp) in self._store.items() if exp < now]
|
71
|
+
for k in expired:
|
72
|
+
self._store.pop(k, None)
|
73
|
+
|
74
|
+
def stop(self):
|
75
|
+
"""Остановить фон очистки"""
|
76
|
+
self._stop_event.set()
|
77
|
+
self._timer.join(timeout=1)
|
@@ -11,4 +11,6 @@ GNServer/GNServer.egg-info/PKG-INFO
|
|
11
11
|
GNServer/GNServer.egg-info/SOURCES.txt
|
12
12
|
GNServer/GNServer.egg-info/dependency_links.txt
|
13
13
|
GNServer/GNServer.egg-info/requires.txt
|
14
|
-
GNServer/GNServer.egg-info/top_level.txt
|
14
|
+
GNServer/GNServer.egg-info/top_level.txt
|
15
|
+
GNServer/GNServer/tools/__init__.py
|
16
|
+
GNServer/GNServer/tools/_models.py
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|