PyMkDB 0.1.0__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.
- pymkdb/__init__.py +6 -0
- pymkdb/cli.py +57 -0
- pymkdb-0.1.0.dist-info/METADATA +86 -0
- pymkdb-0.1.0.dist-info/RECORD +54 -0
- pymkdb-0.1.0.dist-info/WHEEL +5 -0
- pymkdb-0.1.0.dist-info/entry_points.txt +2 -0
- pymkdb-0.1.0.dist-info/top_level.txt +3 -0
- sdk/__init__.py +1 -0
- sdk/connection.py +225 -0
- sdk/delta.py +19 -0
- sdk/http_connection.py +180 -0
- sdk/mkdb_client.py +226 -0
- sdk/responses.py +154 -0
- src/__init__.py +1 -0
- src/config/db.py +227 -0
- src/config/server.py +52 -0
- src/db/__init__.py +207 -0
- src/db/cache/__init__.py +1 -0
- src/db/cache/ram_cache.py +144 -0
- src/db/cache/write_queue.py +156 -0
- src/db/maintenance/__init__.py +0 -0
- src/db/maintenance/compactor.py +118 -0
- src/db/maintenance/task_scheduler.py +73 -0
- src/db/objects/store.py +283 -0
- src/db/parity/__init__.py +0 -0
- src/db/parity/parity_manager.py +196 -0
- src/db/query/__init__.py +1 -0
- src/db/query/full_text_index.py +168 -0
- src/db/query/numeric_index.py +196 -0
- src/db/query/query_engine.py +308 -0
- src/db/query/tokenizer.py +48 -0
- src/db/query_workers/__init__.py +16 -0
- src/db/query_workers/dispatcher.py +339 -0
- src/db/query_workers/task.py +78 -0
- src/db/query_workers/worker.py +292 -0
- src/db/requesting/main.py +0 -0
- src/db/storage/__init__.py +1 -0
- src/db/storage/blob_store.py +47 -0
- src/db/storage/index_manager.py +92 -0
- src/db/storage/log_manager.py +119 -0
- src/db/storage/serializer.py +38 -0
- src/filing/__init__.py +31 -0
- src/objects/__init__.py +190 -0
- src/runtime/__init__.py +15 -0
- src/server/__init__.py +0 -0
- src/server/coms/actions.py +209 -0
- src/server/coms/http.py +46 -0
- src/server/coms/http_handlers.py +445 -0
- src/server/coms/metrics.py +231 -0
- src/server/coms/socket.py +461 -0
- src/server/coms/socket_protocol.py +54 -0
- src/server/control/api/actions.py +1001 -0
- src/server/control/server.py +404 -0
- src/server/event_log.py +58 -0
src/objects/__init__.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
import datetime
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any, Dict, List, Optional, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def formatItems(input:Union[dict, list]) -> Union[dict, list]:
|
|
9
|
+
if type(input) == list:
|
|
10
|
+
data = []
|
|
11
|
+
for item in input:
|
|
12
|
+
if isinstance(item, base_object):
|
|
13
|
+
data.append(item.json)
|
|
14
|
+
elif type(item) in [list, dict]:
|
|
15
|
+
data.append(formatItems(item))
|
|
16
|
+
elif type(item) in [tuple, int, float, str, bool]:
|
|
17
|
+
data.append(item)
|
|
18
|
+
elif callable(item):
|
|
19
|
+
continue
|
|
20
|
+
elif type(input) == dict:
|
|
21
|
+
data = {}
|
|
22
|
+
for key in input:
|
|
23
|
+
if "__" in str(key):
|
|
24
|
+
continue
|
|
25
|
+
item = input[key]
|
|
26
|
+
if isinstance(item, base_object):
|
|
27
|
+
data[key] = item.json
|
|
28
|
+
elif type(item) in [list, dict]:
|
|
29
|
+
data[key] = formatItems(item)
|
|
30
|
+
elif type(item) in [tuple, int, float, str, bool]:
|
|
31
|
+
data[key] = item
|
|
32
|
+
elif callable(item):
|
|
33
|
+
continue
|
|
34
|
+
return data
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class base_object:
|
|
39
|
+
def __init__(self, data:dict={}):
|
|
40
|
+
self.update(data)
|
|
41
|
+
|
|
42
|
+
def get(self, name, default=None):
|
|
43
|
+
return self.__dict__.get(name, default)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def json(self) -> dict:
|
|
47
|
+
data = {}
|
|
48
|
+
for name in self.__dict__:
|
|
49
|
+
try:
|
|
50
|
+
if "__" not in name:
|
|
51
|
+
item = self.__dict__[name]
|
|
52
|
+
if isinstance(item, base_object):
|
|
53
|
+
data[name] = item.json
|
|
54
|
+
elif type(item) in [list, dict]:
|
|
55
|
+
try:
|
|
56
|
+
data[name] = formatItems(item)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
print("error", e)
|
|
59
|
+
print("Is dual ommited", name, type(item), item)
|
|
60
|
+
raise e
|
|
61
|
+
elif type(item) in [tuple, int, float, str, bool, None]:
|
|
62
|
+
data[name] = item
|
|
63
|
+
elif callable(item):
|
|
64
|
+
continue
|
|
65
|
+
else:
|
|
66
|
+
pass
|
|
67
|
+
#print("Is ommited", name, type(item), item)
|
|
68
|
+
#print(data)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
print("base_object EXCEPTION!", name)
|
|
71
|
+
print(e)
|
|
72
|
+
raise e
|
|
73
|
+
return data
|
|
74
|
+
|
|
75
|
+
def update(self, data:dict):
|
|
76
|
+
if type(data) not in [type(None), dict]:
|
|
77
|
+
return
|
|
78
|
+
#raise TypeError(f"base_object requires type 'Dict' on function update, Got: [{type(data)}: {data}]")
|
|
79
|
+
if data is not None and type(data) == dict:
|
|
80
|
+
for name in data:
|
|
81
|
+
item = data[name]
|
|
82
|
+
if name in self.__dict__:
|
|
83
|
+
if isinstance(self.__dict__[name], base_object):
|
|
84
|
+
self.__dict__[name].update(item)
|
|
85
|
+
elif type(item) in [list, int, float, str, dict, bool]:
|
|
86
|
+
self.__setattr__(name, item)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def __repr__(self) -> str:
|
|
90
|
+
return str(self.json) # "base_object: " + json.dumps(self.json)
|
|
91
|
+
|
|
92
|
+
def partition_object(data:Dict[str, str], seperator="."):
|
|
93
|
+
new = {}
|
|
94
|
+
for key in data:
|
|
95
|
+
if seperator in key:
|
|
96
|
+
lv = key.split(seperator)
|
|
97
|
+
i = 0
|
|
98
|
+
gate = new
|
|
99
|
+
for part in lv:
|
|
100
|
+
if i < len(lv)-1:
|
|
101
|
+
if part not in gate:
|
|
102
|
+
gate[part] = {}
|
|
103
|
+
gate = gate[part]
|
|
104
|
+
else:
|
|
105
|
+
gate[part] = data[key]
|
|
106
|
+
i += 1
|
|
107
|
+
else:
|
|
108
|
+
new[key] = data[key]
|
|
109
|
+
return new
|
|
110
|
+
|
|
111
|
+
def compact_object(data:dict, seperator=".", prev=None):
|
|
112
|
+
new = {}
|
|
113
|
+
for key in data:
|
|
114
|
+
if type(data[key]) == dict:
|
|
115
|
+
new.update(compact_object(data[key], seperator, (prev + "." if prev is not None else "") + key))
|
|
116
|
+
else:
|
|
117
|
+
new[(prev + "." if prev is not None else "") + key] = data[key]
|
|
118
|
+
return new
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class runtime_options(base_object):
|
|
122
|
+
def __init__(self, data: dict = {}, parse=True):
|
|
123
|
+
self.args:Union[base_object, dict]
|
|
124
|
+
self.parse_error_exit:bool
|
|
125
|
+
self.parse_types:bool
|
|
126
|
+
self.parse_error_keep:bool
|
|
127
|
+
self.partition_seperator:str
|
|
128
|
+
self.__sys_argv__ = {}
|
|
129
|
+
self.__arg_types__ = {}
|
|
130
|
+
super().__init__(data)
|
|
131
|
+
|
|
132
|
+
for pack in [
|
|
133
|
+
['args', {}],
|
|
134
|
+
['parse_error_exit', False],
|
|
135
|
+
['parse_types', True],
|
|
136
|
+
['parse_error_keep', False],
|
|
137
|
+
['partition_seperator', '.']
|
|
138
|
+
]:
|
|
139
|
+
n, d = pack
|
|
140
|
+
try:
|
|
141
|
+
v = self.__getattribute__(n)
|
|
142
|
+
except:
|
|
143
|
+
v = None
|
|
144
|
+
if v is None:
|
|
145
|
+
self.__setattr__(n, d)
|
|
146
|
+
|
|
147
|
+
if parse:
|
|
148
|
+
self.parse()
|
|
149
|
+
|
|
150
|
+
def __get_types__(self) -> dict[str, type]:
|
|
151
|
+
types = self.args.json if isinstance(self.args, base_object) else self.args
|
|
152
|
+
types = compact_object(types, self.partition_seperator)
|
|
153
|
+
types = {x: type(types[x]) for x in types}
|
|
154
|
+
return types
|
|
155
|
+
|
|
156
|
+
def parse(self):
|
|
157
|
+
import re
|
|
158
|
+
data = {}
|
|
159
|
+
types = self.__get_types__()
|
|
160
|
+
self.__arg_types__ = types
|
|
161
|
+
|
|
162
|
+
# Regex pattern to match key=value pairs
|
|
163
|
+
# Handles: key=value, key="value", key='value', key="value with spaces"
|
|
164
|
+
pattern = r'(\w+[\w\.]*)\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s]*))'
|
|
165
|
+
|
|
166
|
+
matches = re.findall(pattern, ' '.join(sys.argv[1:])) # Skip script name (sys.argv[0])
|
|
167
|
+
for match in matches:
|
|
168
|
+
if match:
|
|
169
|
+
key = match[0]
|
|
170
|
+
# Get the value from whichever group matched (groups 1, 2, or 3)
|
|
171
|
+
value = match[1] or match[2] or match[3] or ""
|
|
172
|
+
data[key] = value
|
|
173
|
+
|
|
174
|
+
self.__sys_argv__ = deepcopy(data)
|
|
175
|
+
|
|
176
|
+
for key in list(data.keys()):
|
|
177
|
+
value = data[key]
|
|
178
|
+
try:
|
|
179
|
+
data[key] = types.get(key)(value) # type: ignore
|
|
180
|
+
except:
|
|
181
|
+
if self.parse_error_exit:
|
|
182
|
+
raise ValueError(f"Could not set param[{key}] to Type of [{types.get(key)}], value: [{value}]")
|
|
183
|
+
else:
|
|
184
|
+
if self.parse_error_keep == False:
|
|
185
|
+
print("Warning:", f"Could not set param[{key}] to Type of [{types.get(key)}], value: [{value}]")
|
|
186
|
+
del data[key]
|
|
187
|
+
|
|
188
|
+
data = partition_object(data, self.partition_seperator)
|
|
189
|
+
if type(self.args) == dict or isinstance(self.args, base_object):
|
|
190
|
+
self.args.update(data)
|
src/runtime/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from src.objects import runtime_options as __runtime_options__
|
|
2
|
+
from src.objects import base_object
|
|
3
|
+
|
|
4
|
+
class runtime_args(base_object):
|
|
5
|
+
def __init__(self, data: dict = {}):
|
|
6
|
+
self.config = "./config.json"
|
|
7
|
+
super().__init__(data)
|
|
8
|
+
|
|
9
|
+
class runtime_options(__runtime_options__):
|
|
10
|
+
def __init__(self, data:dict={}):
|
|
11
|
+
self.args:runtime_args = runtime_args()
|
|
12
|
+
super().__init__(data)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
runtime_settings = runtime_options()
|
src/server/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""
|
|
2
|
+
actions.py — Shared data-plane action executor for MkDB.
|
|
3
|
+
|
|
4
|
+
Both the HTTP and socket transports delegate here for the actual
|
|
5
|
+
read / write / delete / query logic so nothing is duplicated.
|
|
6
|
+
|
|
7
|
+
Usage
|
|
8
|
+
-----
|
|
9
|
+
from src.server.coms.actions import execute, ActionResult
|
|
10
|
+
|
|
11
|
+
result = execute(database, "read", store_name, {"record_id": rid},
|
|
12
|
+
client_key="alice", transport="http")
|
|
13
|
+
if result.ok:
|
|
14
|
+
send_response(result.data)
|
|
15
|
+
else:
|
|
16
|
+
send_error(result.error, result.http_code)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import logging
|
|
21
|
+
import time
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
from typing import Any, Callable, Optional
|
|
24
|
+
|
|
25
|
+
from src.server.coms import metrics as _metrics
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class ActionResult:
|
|
32
|
+
"""Return value from execute()."""
|
|
33
|
+
ok: bool
|
|
34
|
+
data: Any = None # payload on success
|
|
35
|
+
error: str = "" # human-readable message on failure
|
|
36
|
+
http_code: int = 400 # suggested HTTP status code on failure (socket ignores this)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def execute(
|
|
40
|
+
database,
|
|
41
|
+
action: str,
|
|
42
|
+
store_name: str,
|
|
43
|
+
params: dict,
|
|
44
|
+
client_key: str,
|
|
45
|
+
transport: str,
|
|
46
|
+
*,
|
|
47
|
+
on_broadcast: Optional[Callable[[str, dict], None]] = None,
|
|
48
|
+
) -> ActionResult:
|
|
49
|
+
"""
|
|
50
|
+
Execute a data-plane action and return an ActionResult.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
database : active mkdb database instance
|
|
55
|
+
action : "ping" | "read" | "write" | "delete" | "query"
|
|
56
|
+
store_name : target store name (empty string valid only for "ping")
|
|
57
|
+
params : action-specific values —
|
|
58
|
+
read → record_id (str)
|
|
59
|
+
write → record_id (str, optional), delta (dict), bytes_in (int, optional)
|
|
60
|
+
delete → record_id (str)
|
|
61
|
+
query → filter (dict), hydrate (bool)
|
|
62
|
+
client_key : username or IP, forwarded to metrics
|
|
63
|
+
transport : "http" | "socket", forwarded to metrics
|
|
64
|
+
on_broadcast : optional callback(store_name, event_dict) for pub-sub (used by socket)
|
|
65
|
+
"""
|
|
66
|
+
if database is None:
|
|
67
|
+
return ActionResult(ok=False, error="Database not available", http_code=503)
|
|
68
|
+
|
|
69
|
+
if action == "ping":
|
|
70
|
+
return ActionResult(ok=True, data="pong")
|
|
71
|
+
|
|
72
|
+
# All non-ping actions require a named store.
|
|
73
|
+
if not store_name:
|
|
74
|
+
return ActionResult(ok=False, error="'store' is required", http_code=400)
|
|
75
|
+
store_obj = database.stores.get(store_name)
|
|
76
|
+
if store_obj is None:
|
|
77
|
+
return ActionResult(ok=False, error=f"Store '{store_name}' not found", http_code=404)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
if action == "read":
|
|
81
|
+
return _do_read(store_obj, store_name, params, client_key, transport)
|
|
82
|
+
elif action == "write":
|
|
83
|
+
return _do_write(store_obj, store_name, params, client_key, transport, on_broadcast)
|
|
84
|
+
elif action == "delete":
|
|
85
|
+
return _do_delete(store_obj, store_name, params, client_key, transport, on_broadcast)
|
|
86
|
+
elif action == "query":
|
|
87
|
+
return _do_query(store_obj, store_name, params, client_key, transport)
|
|
88
|
+
else:
|
|
89
|
+
return ActionResult(ok=False, error=f"Unknown action: {action!r}", http_code=400)
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
logger.error("Action %r on store %r failed: %s", action, store_name, exc, exc_info=True)
|
|
92
|
+
_metrics.record(store_name, "error", client_key, transport=transport,
|
|
93
|
+
error_msg=f"{type(exc).__name__}: {exc}")
|
|
94
|
+
return ActionResult(ok=False, error=str(exc), http_code=500)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
# Per-action implementations
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def _do_read(store_obj, store_name: str, params: dict, client_key: str, transport: str) -> ActionResult:
|
|
102
|
+
record_id = params.get("record_id", "")
|
|
103
|
+
if not record_id:
|
|
104
|
+
return ActionResult(ok=False, error="Missing 'record_id'", http_code=400)
|
|
105
|
+
|
|
106
|
+
result = store_obj.read(record_id)
|
|
107
|
+
if result is None:
|
|
108
|
+
_metrics.record(store_name, "error", client_key, transport=transport,
|
|
109
|
+
error_msg=f"Record '{record_id}' not found in store '{store_name}'")
|
|
110
|
+
return ActionResult(
|
|
111
|
+
ok=False,
|
|
112
|
+
error=f"Record '{record_id}' not found in store '{store_name}'",
|
|
113
|
+
http_code=404,
|
|
114
|
+
)
|
|
115
|
+
bytes_out = len(json.dumps(result).encode())
|
|
116
|
+
_metrics.record(store_name, "read", client_key, bytes_out=bytes_out, transport=transport)
|
|
117
|
+
return ActionResult(ok=True, data=result)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _do_write(store_obj, store_name: str, params: dict, client_key: str, transport: str,
|
|
121
|
+
on_broadcast) -> ActionResult:
|
|
122
|
+
record_id = str(params.get("record_id", "")).strip()
|
|
123
|
+
delta = params.get("delta", {})
|
|
124
|
+
bytes_in = int(params.get("bytes_in", 0))
|
|
125
|
+
|
|
126
|
+
if not isinstance(delta, dict):
|
|
127
|
+
return ActionResult(ok=False, error="'delta' must be a JSON object", http_code=400)
|
|
128
|
+
|
|
129
|
+
if not record_id:
|
|
130
|
+
try:
|
|
131
|
+
record_id = store_obj.generate_id()
|
|
132
|
+
except RuntimeError as exc:
|
|
133
|
+
return ActionResult(ok=False, error=str(exc), http_code=503)
|
|
134
|
+
|
|
135
|
+
# Fall back to measuring the delta when the transport didn't supply a byte count.
|
|
136
|
+
if not bytes_in:
|
|
137
|
+
bytes_in = len(json.dumps(delta).encode())
|
|
138
|
+
|
|
139
|
+
store_obj.write(record_id, delta)
|
|
140
|
+
_metrics.record(store_name, "write", client_key, bytes_in=bytes_in, transport=transport)
|
|
141
|
+
|
|
142
|
+
if on_broadcast:
|
|
143
|
+
on_broadcast(store_name, {
|
|
144
|
+
"type": "update", "store": store_name,
|
|
145
|
+
"record_id": record_id, "op": "write",
|
|
146
|
+
})
|
|
147
|
+
return ActionResult(ok=True, data={"record_id": record_id})
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _do_delete(store_obj, store_name: str, params: dict, client_key: str, transport: str,
|
|
151
|
+
on_broadcast) -> ActionResult:
|
|
152
|
+
record_id = params.get("record_id", "")
|
|
153
|
+
if not record_id:
|
|
154
|
+
return ActionResult(ok=False, error="Missing 'record_id'", http_code=400)
|
|
155
|
+
|
|
156
|
+
store_obj.delete(record_id)
|
|
157
|
+
_metrics.record(store_name, "delete", client_key, transport=transport)
|
|
158
|
+
|
|
159
|
+
if on_broadcast:
|
|
160
|
+
on_broadcast(store_name, {
|
|
161
|
+
"type": "update", "store": store_name,
|
|
162
|
+
"record_id": record_id, "op": "delete",
|
|
163
|
+
})
|
|
164
|
+
return ActionResult(ok=True, data={"record_id": record_id})
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _do_query(store_obj, store_name: str, params: dict, client_key: str, transport: str) -> ActionResult:
|
|
168
|
+
filter_dict = params.get("filter", {})
|
|
169
|
+
hydrate = bool(params.get("hydrate", False))
|
|
170
|
+
|
|
171
|
+
if not isinstance(filter_dict, dict):
|
|
172
|
+
return ActionResult(ok=False, error="'filter' must be a JSON object", http_code=400)
|
|
173
|
+
|
|
174
|
+
qe = getattr(store_obj, "query_engine", None)
|
|
175
|
+
if qe is None:
|
|
176
|
+
return ActionResult(ok=False, error=f"Store '{store_name}' has no query engine", http_code=500)
|
|
177
|
+
|
|
178
|
+
t0 = time.monotonic()
|
|
179
|
+
ids = qe.query(filter_dict)
|
|
180
|
+
duration_ms = (time.monotonic() - t0) * 1000.0
|
|
181
|
+
|
|
182
|
+
if hydrate:
|
|
183
|
+
records = [store_obj.read(rid) for rid in ids]
|
|
184
|
+
result = {"count": len(records), "records": records}
|
|
185
|
+
else:
|
|
186
|
+
result = {"count": len(ids), "ids": ids}
|
|
187
|
+
|
|
188
|
+
bytes_out = len(json.dumps(result).encode())
|
|
189
|
+
_metrics.record(store_name, "query", client_key, bytes_out=bytes_out, transport=transport)
|
|
190
|
+
|
|
191
|
+
# Slow query detection
|
|
192
|
+
threshold_ms = getattr(getattr(store_obj, "config", None), "slow_query_threshold_ms", 0.0)
|
|
193
|
+
if threshold_ms > 0 and duration_ms >= threshold_ms:
|
|
194
|
+
_metrics.record_slow_query(
|
|
195
|
+
store_name, duration_ms, filter_dict,
|
|
196
|
+
result.get("count", 0), client_key, transport,
|
|
197
|
+
)
|
|
198
|
+
try:
|
|
199
|
+
from src.server.event_log import emit as _emit
|
|
200
|
+
_emit(
|
|
201
|
+
"warning", f"store:{store_name}",
|
|
202
|
+
f"Slow query ({duration_ms:.1f} ms ≥ {threshold_ms:.0f} ms threshold): "
|
|
203
|
+
f"filter={json.dumps(filter_dict)}",
|
|
204
|
+
)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
result["duration_ms"] = round(duration_ms, 2)
|
|
209
|
+
return ActionResult(ok=True, data=result)
|
src/server/coms/http.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer as _HTTPServer
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HTTPServer(_HTTPServer):
|
|
11
|
+
"""Production-ready HTTP server with multi-threaded request handling."""
|
|
12
|
+
|
|
13
|
+
def __init__(self, name:str, host:str, port:int, responder:BaseHTTPRequestHandler):
|
|
14
|
+
"""Initialize HTTP server.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name: Server name
|
|
18
|
+
host: Server host address
|
|
19
|
+
port: Server port
|
|
20
|
+
"""
|
|
21
|
+
self.host = host
|
|
22
|
+
self.port = port
|
|
23
|
+
self.name = name
|
|
24
|
+
self.logger = logging.getLogger(f"HTTPServer-{name}")
|
|
25
|
+
super().__init__((host, port), responder) #type: ignore
|
|
26
|
+
self.logger.info(f"HTTP Server initialized on {host}:{port}")
|
|
27
|
+
threading.Thread(target=self.run, daemon=True).start()
|
|
28
|
+
|
|
29
|
+
def run(self) -> None:
|
|
30
|
+
"""Start the HTTP server."""
|
|
31
|
+
print(f"Starting {self.name} HTTP Server on {self.host}:{self.port}")
|
|
32
|
+
self.logger.info(f"Starting HTTP Server on {self.host}:{self.port}")
|
|
33
|
+
try:
|
|
34
|
+
self.serve_forever()
|
|
35
|
+
except KeyboardInterrupt:
|
|
36
|
+
self.logger.info("Server interrupted by user")
|
|
37
|
+
self.shutdown()
|
|
38
|
+
except Exception as e:
|
|
39
|
+
self.logger.error(f"Server error: {e}", exc_info=True)
|
|
40
|
+
self.shutdown()
|
|
41
|
+
|
|
42
|
+
def shutdown(self) -> None:
|
|
43
|
+
"""Gracefully shutdown the server."""
|
|
44
|
+
self.logger.info("Shutting down HTTP Server")
|
|
45
|
+
super().shutdown() # signals serve_forever() to stop
|
|
46
|
+
self.server_close() # closes the server socket
|