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.
Files changed (54) hide show
  1. pymkdb/__init__.py +6 -0
  2. pymkdb/cli.py +57 -0
  3. pymkdb-0.1.0.dist-info/METADATA +86 -0
  4. pymkdb-0.1.0.dist-info/RECORD +54 -0
  5. pymkdb-0.1.0.dist-info/WHEEL +5 -0
  6. pymkdb-0.1.0.dist-info/entry_points.txt +2 -0
  7. pymkdb-0.1.0.dist-info/top_level.txt +3 -0
  8. sdk/__init__.py +1 -0
  9. sdk/connection.py +225 -0
  10. sdk/delta.py +19 -0
  11. sdk/http_connection.py +180 -0
  12. sdk/mkdb_client.py +226 -0
  13. sdk/responses.py +154 -0
  14. src/__init__.py +1 -0
  15. src/config/db.py +227 -0
  16. src/config/server.py +52 -0
  17. src/db/__init__.py +207 -0
  18. src/db/cache/__init__.py +1 -0
  19. src/db/cache/ram_cache.py +144 -0
  20. src/db/cache/write_queue.py +156 -0
  21. src/db/maintenance/__init__.py +0 -0
  22. src/db/maintenance/compactor.py +118 -0
  23. src/db/maintenance/task_scheduler.py +73 -0
  24. src/db/objects/store.py +283 -0
  25. src/db/parity/__init__.py +0 -0
  26. src/db/parity/parity_manager.py +196 -0
  27. src/db/query/__init__.py +1 -0
  28. src/db/query/full_text_index.py +168 -0
  29. src/db/query/numeric_index.py +196 -0
  30. src/db/query/query_engine.py +308 -0
  31. src/db/query/tokenizer.py +48 -0
  32. src/db/query_workers/__init__.py +16 -0
  33. src/db/query_workers/dispatcher.py +339 -0
  34. src/db/query_workers/task.py +78 -0
  35. src/db/query_workers/worker.py +292 -0
  36. src/db/requesting/main.py +0 -0
  37. src/db/storage/__init__.py +1 -0
  38. src/db/storage/blob_store.py +47 -0
  39. src/db/storage/index_manager.py +92 -0
  40. src/db/storage/log_manager.py +119 -0
  41. src/db/storage/serializer.py +38 -0
  42. src/filing/__init__.py +31 -0
  43. src/objects/__init__.py +190 -0
  44. src/runtime/__init__.py +15 -0
  45. src/server/__init__.py +0 -0
  46. src/server/coms/actions.py +209 -0
  47. src/server/coms/http.py +46 -0
  48. src/server/coms/http_handlers.py +445 -0
  49. src/server/coms/metrics.py +231 -0
  50. src/server/coms/socket.py +461 -0
  51. src/server/coms/socket_protocol.py +54 -0
  52. src/server/control/api/actions.py +1001 -0
  53. src/server/control/server.py +404 -0
  54. src/server/event_log.py +58 -0
@@ -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)
@@ -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)
@@ -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