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
sdk/mkdb_client.py ADDED
@@ -0,0 +1,226 @@
1
+ """
2
+ MkDB high-level SDK client.
3
+
4
+ Usage:
5
+ from sdk.mkdb_client import MkDBClient
6
+
7
+ client = MkDBClient(host="127.0.0.1", port=9001)
8
+ client.connect()
9
+
10
+ client.set("products_1v", "p001", {"name": "Widget", "price": 9.99})
11
+ record = client.get("products_1v", "p001")
12
+ print(record)
13
+
14
+ results = client.query("products_1v", {"price": {"gte": 5, "lte": 20}})
15
+ print(results)
16
+
17
+ client.on_update("products_1v", lambda event: print("Change:", event))
18
+ client.close()
19
+ """
20
+
21
+ import hmac
22
+ import hashlib
23
+ import secrets
24
+ from typing import Callable, Optional
25
+
26
+ from sdk.connection import Connection
27
+ from sdk.http_connection import HttpConnection
28
+ from sdk.delta import flatten
29
+ from sdk.responses import (
30
+ GetResponse,
31
+ WriteResponse,
32
+ DeleteResponse,
33
+ QueryResponse,
34
+ GenerateIdResponse,
35
+ )
36
+
37
+
38
+ class MkDBClient:
39
+ def __init__(self, host: str = "127.0.0.1", port: int = 9001,
40
+ access: str = "R",
41
+ username: str = "",
42
+ password: str = "mk_db",
43
+ recv_timeout: float = 30.0,
44
+ transport: str = "socket"):
45
+ """
46
+ Parameters
47
+ ----------
48
+ host, port : server address
49
+ access : "R", "W", or "RW"
50
+ username : username (required if server has users configured)
51
+ password : password / write key
52
+ recv_timeout : seconds to wait for a response
53
+ transport : "socket" (default) or "http"
54
+ HTTP transport does not support pub-sub (on_update).
55
+ """
56
+ t = transport.lower()
57
+ if t == "http":
58
+ self._conn = HttpConnection(host=host, port=port, recv_timeout=recv_timeout,
59
+ access=access, username=username, password=password)
60
+ else:
61
+ self._conn = Connection(host=host, port=port, recv_timeout=recv_timeout,
62
+ access=access, username=username, password=password)
63
+ self.auth = AuthManager(self)
64
+
65
+ # ------------------------------------------------------------------
66
+ # Lifecycle
67
+ # ------------------------------------------------------------------
68
+
69
+ def connect(self) -> None:
70
+ self._conn.connect()
71
+
72
+ def close(self) -> None:
73
+ self._conn.close()
74
+
75
+ # ------------------------------------------------------------------
76
+ # Data operations
77
+ # ------------------------------------------------------------------
78
+
79
+ def get(self, store: str, record_id: str) -> GetResponse:
80
+ """Read a record. Returns a GetResponse (check .found before using .data)."""
81
+ resp = self._conn.send({
82
+ "action": "read",
83
+ "store": store,
84
+ "record_id": record_id,
85
+ })
86
+ self._raise_if_error(resp)
87
+ data = resp.get("data")
88
+ return GetResponse(record_id=record_id, data=data, found=data is not None)
89
+
90
+ def set(self, store: str, record_id: str, delta: dict,
91
+ flatten_nested: bool = True) -> WriteResponse:
92
+ """Write / update a record. Returns a WriteResponse."""
93
+ flat = flatten(delta) if flatten_nested else delta
94
+ resp = self._conn.send({
95
+ "action": "write",
96
+ "store": store,
97
+ "record_id": record_id,
98
+ "delta": flat,
99
+ })
100
+ self._raise_if_error(resp)
101
+ rid = (resp.get("data") or {}).get("record_id", record_id)
102
+ return WriteResponse(record_id=rid, store=store)
103
+
104
+ def insert(self, store: str, delta: dict,
105
+ flatten_nested: bool = True) -> WriteResponse:
106
+ """Write a new record with a server-generated ID. Returns a WriteResponse."""
107
+ flat = flatten(delta) if flatten_nested else delta
108
+ resp = self._conn.send({
109
+ "action": "write",
110
+ "store": store,
111
+ "delta": flat,
112
+ })
113
+ self._raise_if_error(resp)
114
+ rid = (resp.get("data") or {}).get("record_id", "")
115
+ return WriteResponse(record_id=rid, store=store)
116
+
117
+ def generate_id(self, store: str) -> GenerateIdResponse:
118
+ """Ask the server to generate a unique ID for the store without writing anything."""
119
+ resp = self._conn.send({
120
+ "action": "generate_id",
121
+ "store": store,
122
+ })
123
+ self._raise_if_error(resp)
124
+ rid = (resp.get("data") or {}).get("record_id", "")
125
+ return GenerateIdResponse(record_id=rid, store=store)
126
+
127
+ def delete(self, store: str, record_id: str) -> DeleteResponse:
128
+ """Delete a record. Returns a DeleteResponse."""
129
+ resp = self._conn.send({
130
+ "action": "delete",
131
+ "store": store,
132
+ "record_id": record_id,
133
+ })
134
+ self._raise_if_error(resp)
135
+ return DeleteResponse(record_id=record_id, store=store)
136
+
137
+ def query(self, store: str, filter_dict: dict,
138
+ hydrate: bool = False) -> QueryResponse:
139
+ """Query a store. Returns a QueryResponse."""
140
+ resp = self._conn.send({
141
+ "action": "query",
142
+ "store": store,
143
+ "filter": filter_dict,
144
+ "hydrate": hydrate,
145
+ })
146
+ self._raise_if_error(resp)
147
+ data = resp.get("data") or {}
148
+ return QueryResponse(
149
+ count=data.get("count", 0),
150
+ ids=data.get("ids", []),
151
+ records=data.get("records"),
152
+ store=store,
153
+ )
154
+
155
+ # ------------------------------------------------------------------
156
+ # Pub-sub
157
+ # ------------------------------------------------------------------
158
+
159
+ def on_update(self, store: str, callback: Callable[[dict], None]) -> None:
160
+ """
161
+ Subscribe to updates for a store and register a callback for push events.
162
+ The callback receives the full server-push event dict.
163
+ """
164
+ def _filter(event: dict) -> None:
165
+ if event.get("store") == store:
166
+ callback(event)
167
+ self._conn.register_push_handler(_filter)
168
+ self._conn.send_raw({"type": "subscribe", "store": store})
169
+
170
+ # ------------------------------------------------------------------
171
+ # Internal helpers
172
+ # ------------------------------------------------------------------
173
+
174
+ @staticmethod
175
+ def _raise_if_error(resp: dict) -> None:
176
+ if resp.get("status") != "ok":
177
+ raise RuntimeError(resp.get("error", "Unknown error"))
178
+
179
+
180
+ class AuthManager:
181
+ """
182
+ Simple credential store using MkDB itself as the backing store.
183
+
184
+ Passwords are never stored in plaintext.
185
+ Storage format (flat dict in MkDB):
186
+ {"auth.salt": "<hex>", "auth.hash": "<sha256 hex>"}
187
+ Stored under store="__auth__", record_id=username.
188
+ """
189
+
190
+ AUTH_STORE = "__auth__"
191
+
192
+ def __init__(self, client: MkDBClient):
193
+ self._client = client
194
+
195
+ def register(self, username: str, password: str) -> None:
196
+ """
197
+ Register a new user. Raises RuntimeError if username already exists
198
+ or if the __auth__ store is unavailable.
199
+ """
200
+ salt = secrets.token_hex(32)
201
+ pw_hash = hashlib.sha256(
202
+ (password + salt).encode("utf-8")
203
+ ).hexdigest()
204
+ self._client.set(
205
+ self.AUTH_STORE,
206
+ username,
207
+ {"auth.salt": salt, "auth.hash": pw_hash},
208
+ flatten_nested=False,
209
+ )
210
+
211
+ def validate(self, username: str, password: str) -> bool:
212
+ """
213
+ Validate credentials. Returns True if valid, False otherwise.
214
+ Uses hmac.compare_digest to prevent timing attacks.
215
+ """
216
+ record = self._client.get(self.AUTH_STORE, username)
217
+ if record is None:
218
+ # Use compare_digest anyway to avoid timing oracle
219
+ hmac.compare_digest("x", "y")
220
+ return False
221
+ stored_salt = record.get("auth.salt", "")
222
+ stored_hash = record.get("auth.hash", "")
223
+ computed = hashlib.sha256(
224
+ (password + stored_salt).encode("utf-8")
225
+ ).hexdigest()
226
+ return hmac.compare_digest(computed, stored_hash)
sdk/responses.py ADDED
@@ -0,0 +1,154 @@
1
+ """
2
+ MkDB SDK response objects.
3
+
4
+ Every public client method returns one of these instead of a raw dict,
5
+ giving callers full IDE autocompletion and type-checking.
6
+
7
+ Entity data (the actual record fields) is intentionally left as plain
8
+ ``dict`` — no generated model classes.
9
+ """
10
+
11
+ from __future__ import annotations
12
+ from typing import Optional, Union
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Minimal base_object — copied from src/objects so the SDK stays standalone.
17
+ # Provides .json and .update() exactly as the server-side version does.
18
+ # ---------------------------------------------------------------------------
19
+
20
+ def _format_items(input: Union[dict, list]) -> Union[dict, list]:
21
+ if type(input) == list:
22
+ data = []
23
+ for item in input:
24
+ if isinstance(item, base_object):
25
+ data.append(item.json)
26
+ elif type(item) in [list, dict]:
27
+ data.append(_format_items(item))
28
+ elif type(item) in [tuple, int, float, str, bool]:
29
+ data.append(item)
30
+ return data
31
+ elif type(input) == dict:
32
+ data = {}
33
+ for key in input:
34
+ if "__" in str(key):
35
+ continue
36
+ item = input[key]
37
+ if isinstance(item, base_object):
38
+ data[key] = item.json
39
+ elif type(item) in [list, dict]:
40
+ data[key] = _format_items(item)
41
+ elif type(item) in [tuple, int, float, str, bool]:
42
+ data[key] = item
43
+ return data
44
+ return input
45
+
46
+
47
+ class base_object:
48
+ def __init__(self, data: dict = {}):
49
+ self.update(data)
50
+
51
+ def get(self, name, default=None):
52
+ return self.__dict__.get(name, default)
53
+
54
+ @property
55
+ def json(self) -> dict:
56
+ data = {}
57
+ for name in self.__dict__:
58
+ if "__" in name:
59
+ continue
60
+ item = self.__dict__[name]
61
+ if isinstance(item, base_object):
62
+ data[name] = item.json
63
+ elif type(item) in [list, dict]:
64
+ data[name] = _format_items(item)
65
+ elif type(item) in [tuple, int, float, str, bool]:
66
+ data[name] = item
67
+ return data
68
+
69
+ def update(self, data: dict):
70
+ if not isinstance(data, dict):
71
+ return
72
+ for name, item in data.items():
73
+ if name in self.__dict__:
74
+ if isinstance(self.__dict__[name], base_object):
75
+ self.__dict__[name].update(item)
76
+ elif type(item) in [list, int, float, str, dict, bool]:
77
+ setattr(self, name, item)
78
+
79
+ def __repr__(self) -> str:
80
+ return str(self.json)
81
+
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Response classes
85
+ # ---------------------------------------------------------------------------
86
+
87
+
88
+ class GetResponse(base_object):
89
+ """Returned by ``client.get()``."""
90
+
91
+ def __init__(self, record_id: str, data: Optional[dict], found: bool):
92
+ self.record_id: str = record_id
93
+ self.data: Optional[dict] = data # flat field dict, or None
94
+ self.found: bool = found
95
+ super().__init__({})
96
+
97
+ def __bool__(self) -> bool:
98
+ return self.found
99
+
100
+
101
+ class WriteResponse(base_object):
102
+ """Returned by ``client.set()`` and ``client.insert()``."""
103
+
104
+ def __init__(self, record_id: str, store: str):
105
+ self.record_id: str = record_id
106
+ self.store: str = store
107
+ super().__init__({})
108
+
109
+
110
+ class DeleteResponse(base_object):
111
+ """Returned by ``client.delete()``."""
112
+
113
+ def __init__(self, record_id: str, store: str):
114
+ self.record_id: str = record_id
115
+ self.store: str = store
116
+ super().__init__({})
117
+
118
+
119
+ class QueryResponse(base_object):
120
+ """Returned by ``client.query()``."""
121
+
122
+ def __init__(self, count: int, ids: list,
123
+ records: Optional[list] = None,
124
+ store: str = ""):
125
+ self.count: int = count
126
+ self.ids: list = ids
127
+ # Only populated when hydrate=True
128
+ self.records: Optional[list] = records
129
+ self.store: str = store
130
+ super().__init__({})
131
+
132
+ def __len__(self) -> int:
133
+ return self.count
134
+
135
+ def __bool__(self) -> bool:
136
+ return self.count > 0
137
+
138
+ def __iter__(self):
139
+ """Iterate over records if hydrated, otherwise over IDs."""
140
+ if self.records is not None:
141
+ return iter(self.records)
142
+ return iter(self.ids)
143
+
144
+
145
+ class GenerateIdResponse(base_object):
146
+ """Returned by ``client.generate_id()``."""
147
+
148
+ def __init__(self, record_id: str, store: str):
149
+ self.record_id: str = record_id
150
+ self.store: str = store
151
+ super().__init__({})
152
+
153
+ def __str__(self) -> str:
154
+ return self.record_id
src/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # MkDB core engine package
src/config/db.py ADDED
@@ -0,0 +1,227 @@
1
+ import os
2
+
3
+ from src.filing import write_json
4
+ from src.config.server import control_server, socket_server, http_server
5
+ from src.objects import base_object
6
+
7
+ class query_worker_config(base_object):
8
+ def __init__(self, data:dict={}):
9
+ self.parallel_enabled = False # set True to spin up worker processes
10
+ self.worker_count = 0 # 0 = auto (os.cpu_count())
11
+ self.worker_cache_size = 500 # max records in each worker's local cache
12
+ self.worker_cache_ttl = 30 # seconds before a worker cache entry expires
13
+ self.task_timeout = 30.0 # seconds before submit() raises TimeoutError
14
+ super().__init__(data)
15
+
16
+ class ram_config(base_object):
17
+ def __init__(self, data:dict={}):
18
+ self.max_size = 1000
19
+ self.ttl = 3600
20
+ self.cleanup_interval = 600
21
+ self.clean_type = "lru"
22
+ self.update_on_access = True
23
+ self.index = True
24
+ self.index_cache_ttl = 3600
25
+ self.query_fields = True
26
+ self.query_fields_cache_ttl = 3600
27
+ # 0 = keep all query indexes in RAM.
28
+ # >0 = if an index file on disk exceeds this many bytes, don't load
29
+ # it into RAM; query and write directly from/to the file instead.
30
+ self.index_ram_threshold_bytes = 0
31
+ super().__init__(data)
32
+
33
+ TOKEN_SOFT_LIMIT = 28 # health = "low" when auto_expand is on and length >= this
34
+ TOKEN_HARD_LIMIT = 32 # health = "degraded" regardless; performance warning threshold
35
+
36
+ class entity_config(base_object):
37
+ def __init__(self, data:dict={}):
38
+ self.token_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
39
+ self.token_length = 16
40
+ self.auto_expand = False
41
+ super().__init__(data)
42
+
43
+ @property
44
+ def health(self) -> dict:
45
+ """Return entity-config health status and an explanatory message."""
46
+ if self.token_length > TOKEN_HARD_LIMIT:
47
+ return {
48
+ "status": "degraded",
49
+ "message": (
50
+ f"Token length {self.token_length} exceeds the recommended "
51
+ f"maximum of {TOKEN_HARD_LIMIT} characters. "
52
+ "Performance may degrade — consider reducing it."
53
+ ),
54
+ }
55
+ if self.auto_expand and self.token_length >= TOKEN_SOFT_LIMIT:
56
+ return {
57
+ "status": "low",
58
+ "message": (
59
+ f"Token length {self.token_length} is approaching the "
60
+ f"{TOKEN_HARD_LIMIT}-character performance limit. "
61
+ "With auto expand enabled this may increase further, "
62
+ "degrading database performance."
63
+ ),
64
+ }
65
+ return {"status": "ok", "message": ""}
66
+
67
+ class file_config(base_object):
68
+ def __init__(self, data:dict={}):
69
+ self.file_size_split_trigger = 20000000
70
+ self.type = "rolling_log"
71
+ self.blob_threshold = 5 * 1024 * 1024 # 5 MB
72
+ self.segment_threshold = 50 * 1024 * 1024 # 50 MB
73
+ self.parity_nsym = 10
74
+ self.compaction_dead_ratio = 0.3
75
+ self.compaction_interval = 60
76
+ super().__init__(data)
77
+
78
+ class field_schema(base_object):
79
+ def __init__(self, data={}):
80
+ self.name = ""
81
+ self.queryable = False # False | "exact" | "full-text" | "numeric"
82
+ self.indexed = False
83
+ super().__init__(data)
84
+
85
+ class schema_config(base_object):
86
+ def __init__(self, data={}):
87
+ self.fields: dict = {}
88
+ super().__init__(data)
89
+ self._normalise_fields()
90
+
91
+ def _normalise_fields(self):
92
+ self.fields = {
93
+ k: field_schema(v) if isinstance(v, dict) else v
94
+ for k, v in self.fields.items()
95
+ }
96
+
97
+ def update(self, data: dict):
98
+ super().update(data)
99
+ self._normalise_fields()
100
+
101
+ class write_queue_config(base_object):
102
+ def __init__(self, data={}):
103
+ self.debounce_window = 5.0
104
+ self.max_pending = 10000
105
+ super().__init__(data)
106
+
107
+ class store_rate_limit(base_object):
108
+ """Per-store IP rate limit for the HTTP data plane."""
109
+ def __init__(self, data: dict = {}):
110
+ self.enabled = False
111
+ self.max_requests_per_second = 100
112
+ super().__init__(data)
113
+
114
+ class store_config(base_object):
115
+ def __init__(self, data:dict={}):
116
+ self.name = ""
117
+ self.description = ""
118
+ self.protect_reads = False # when True, read/query also require auth
119
+ self.slow_query_threshold_ms = 0.0 # 0 = disabled; >0 = log queries slower than X ms
120
+ self.client_id_header = "" # HTTP header to use as client identifier; "" = remote_addr
121
+ self.file_config = file_config(data.get("file_config", {}))
122
+ self.entity_config = entity_config(data.get("entity_config", {}))
123
+ self.ram_config = ram_config(data.get("ram_config", {}))
124
+ self.query_worker_config = query_worker_config(data.get("query_worker_config", {}))
125
+ self.schema_config = schema_config(data.get("schema_config", {}))
126
+ self.write_queue_config = write_queue_config(data.get("write_queue_config", {}))
127
+ self.rate_limit = store_rate_limit(data.get("rate_limit", {}))
128
+ super().__init__(data)
129
+
130
+ class backup_config(base_object):
131
+ def __init__(self, data:dict={}):
132
+ self.enabled = False
133
+ self.interval = 3600
134
+ self.directory = "backups"
135
+ super().__init__(data)
136
+
137
+ class store_permission(base_object):
138
+ """Read/write permissions for a user on a specific store."""
139
+ def __init__(self, data: dict = {}):
140
+ self.read = True
141
+ self.write = False
142
+ super().__init__(data)
143
+
144
+ class db_user(base_object):
145
+ """A database user who can authenticate against the data plane."""
146
+ def __init__(self, data: dict = {}):
147
+ self.username = ""
148
+ self.password_hash = ""
149
+ self.stores: dict = {} # store_name -> store_permission
150
+ super().__init__(data)
151
+ self.stores = {
152
+ k: store_permission(v) if isinstance(v, dict) else v
153
+ for k, v in self.stores.items()
154
+ }
155
+
156
+ class data_security_config(base_object):
157
+ """Security settings for the data plane (HTTP + Socket)."""
158
+ def __init__(self, data: dict = {}):
159
+ # When True, GET/read/query requests also require authentication.
160
+ # When False (default), only write/delete requests require auth.
161
+ self.protect_reads = False
162
+ super().__init__(data)
163
+
164
+ # ── Control-plane RBAC ───────────────────────────────────────────────────────
165
+ # Roles (least → most privileged):
166
+ # viewer – read-only: list stores, get configs, view logs
167
+ # operator – viewer + create/delete stores, update store configs
168
+ # admin – operator + all control settings (auth, users, server config)
169
+
170
+ CONTROL_ROLES = ("viewer", "operator", "admin")
171
+
172
+ class control_user(base_object):
173
+ """A control-plane SDK user with a role."""
174
+ def __init__(self, data: dict = {}):
175
+ self.username = ""
176
+ self.password_hash = ""
177
+ self.role = "viewer" # "viewer" | "operator" | "admin"
178
+ super().__init__(data)
179
+
180
+ class mkdb_servers_config(base_object):
181
+ def __init__(self, data:dict={}):
182
+ self.control_server = control_server(data.get("control_server", {}))
183
+ self.socket_server = socket_server(data.get("socket_server", {}))
184
+ self.http_server = http_server(data.get("http_server", {}))
185
+ super().__init__(data)
186
+
187
+ class mkdb_config(base_object):
188
+ def __init__(self, data:dict={}):
189
+ self.name = ""
190
+ self.storage_nodes: list = []
191
+ self.stores:dict[str, store_config] = {}
192
+ super().__init__(data)
193
+ self.servers = mkdb_servers_config(data.get("servers", {}))
194
+ self.backup = backup_config(data.get("backup", {}))
195
+ for store_name, store_data in data.get("stores", {}).items():
196
+ self.stores[store_name] = store_config(store_data)
197
+ self.users: dict = {} # username -> db_user
198
+ for uname, udata in data.get("users", {}).items():
199
+ self.users[uname] = db_user(udata)
200
+ self.data_security = data_security_config(data.get("data_security", {}))
201
+ self.control_users: dict = {} # username -> control_user
202
+ for uname, udata in data.get("control_users", {}).items():
203
+ self.control_users[uname] = control_user(udata)
204
+
205
+ @property
206
+ def base_path(self):
207
+ if not hasattr(self, "__base_path__"):
208
+ self.__base_path__ = os.getcwd()
209
+ return self.__base_path__
210
+
211
+ def new_store(self, store_name:str, description:str=""):
212
+ """Add a new store to the configuration."""
213
+ if store_name in self.stores:
214
+ raise ValueError(f"Store '{store_name}' already exists in the configuration.")
215
+
216
+ new_store_config = store_config()
217
+ new_store_config.name = store_name
218
+ new_store_config.description = description
219
+ self.stores[store_name] = new_store_config
220
+ print(f"Store '{store_name}' added to configuration.")
221
+ self.save()
222
+
223
+ def save(self):
224
+ """Save the current configuration to a file."""
225
+ config_path = os.path.join(self.base_path, "config.json")
226
+ write_json(config_path, self.json)
227
+ print(f"Configuration saved to {config_path}")
src/config/server.py ADDED
@@ -0,0 +1,52 @@
1
+ from src.objects import base_object
2
+
3
+ class server_address_config(base_object):
4
+ def __init__(self, data:dict={}):
5
+ self.host = ""
6
+ self.port = 0
7
+ super().__init__(data)
8
+
9
+
10
+ class auth_config(base_object):
11
+ """Authentication configuration shared by all server types."""
12
+ def __init__(self, data:dict={}):
13
+ self.enabled = False
14
+ self.password_hash = "" # empty = no password set; format: pbkdf2:sha256:{iters}:{salt}:{hash}
15
+ self.session_ttl = 3600 # seconds until a session token expires
16
+ super().__init__(data)
17
+
18
+
19
+ class control_server(base_object):
20
+ def __init__(self, data:dict={}):
21
+ self.enabled = True
22
+ self.address = server_address_config(data.get("address", {}))
23
+ self.auth = auth_config(data.get("auth", {}))
24
+ super().__init__(data)
25
+ if self.enabled and self.address.host in [None, ""]:
26
+ print("Control server enabled but no host specified. Defaulting to localhost.")
27
+ self.address.host = "localhost"
28
+ if self.enabled and self.address.port == 0:
29
+ print("Control server enabled but no port specified. Defaulting to port 80.")
30
+ self.address.port = 80
31
+
32
+
33
+ class socket_server(base_object):
34
+ def __init__(self, data:dict={}):
35
+ self.enabled = True
36
+ self.address = server_address_config(data.get("address", {}))
37
+ self.auth = auth_config(data.get("auth", {}))
38
+ self.heartbeat_interval = 5.0
39
+ self.max_clients = 100
40
+ self.recv_timeout = 30.0
41
+ super().__init__(data)
42
+
43
+ class http_server(base_object):
44
+ def __init__(self, data:dict={}):
45
+ self.enabled = False
46
+ self.address = server_address_config(data.get("address", {}))
47
+ self.auth = auth_config(data.get("auth", {}))
48
+ self.max_body_size = 10 * 1024 * 1024
49
+ self.cors_enabled = False
50
+ self.cors_origins: list = ["*"]
51
+ self.max_requests_per_second = 100
52
+ super().__init__(data)