sovereign 0.19.3__py3-none-any.whl → 1.0.0a4__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.
Potentially problematic release.
This version of sovereign might be problematic. Click here for more details.
- sovereign/__init__.py +13 -81
- sovereign/app.py +62 -48
- sovereign/cache/__init__.py +245 -0
- sovereign/cache/backends/__init__.py +110 -0
- sovereign/cache/backends/s3.py +161 -0
- sovereign/cache/filesystem.py +74 -0
- sovereign/cache/types.py +17 -0
- sovereign/configuration.py +607 -0
- sovereign/constants.py +1 -0
- sovereign/context.py +270 -104
- sovereign/dynamic_config/__init__.py +112 -0
- sovereign/dynamic_config/deser.py +78 -0
- sovereign/dynamic_config/loaders.py +120 -0
- sovereign/error_info.py +2 -3
- sovereign/events.py +49 -0
- sovereign/logging/access_logger.py +85 -0
- sovereign/logging/application_logger.py +54 -0
- sovereign/logging/base_logger.py +41 -0
- sovereign/logging/bootstrapper.py +36 -0
- sovereign/logging/types.py +10 -0
- sovereign/middlewares.py +8 -7
- sovereign/modifiers/lib.py +2 -1
- sovereign/rendering.py +124 -0
- sovereign/rendering_common.py +91 -0
- sovereign/response_class.py +18 -0
- sovereign/server.py +112 -35
- sovereign/statistics.py +19 -21
- sovereign/templates/base.html +59 -46
- sovereign/templates/resources.html +203 -102
- sovereign/testing/loaders.py +9 -0
- sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
- sovereign/tracing.py +103 -0
- sovereign/types.py +304 -0
- sovereign/utils/auth.py +27 -13
- sovereign/utils/crypto/__init__.py +0 -0
- sovereign/utils/crypto/crypto.py +135 -0
- sovereign/utils/crypto/suites/__init__.py +21 -0
- sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
- sovereign/utils/crypto/suites/base_cipher.py +21 -0
- sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
- sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
- sovereign/utils/dictupdate.py +3 -2
- sovereign/utils/eds.py +40 -22
- sovereign/utils/entry_point_loader.py +2 -2
- sovereign/utils/mock.py +56 -17
- sovereign/utils/resources.py +17 -0
- sovereign/utils/templates.py +4 -2
- sovereign/utils/timer.py +5 -3
- sovereign/utils/version_info.py +8 -0
- sovereign/utils/weighted_clusters.py +2 -1
- sovereign/v2/__init__.py +0 -0
- sovereign/v2/data/data_store.py +621 -0
- sovereign/v2/data/render_discovery_response.py +24 -0
- sovereign/v2/data/repositories.py +90 -0
- sovereign/v2/data/utils.py +33 -0
- sovereign/v2/data/worker_queue.py +273 -0
- sovereign/v2/jobs/refresh_context.py +117 -0
- sovereign/v2/jobs/render_discovery_job.py +145 -0
- sovereign/v2/logging.py +81 -0
- sovereign/v2/types.py +41 -0
- sovereign/v2/web.py +101 -0
- sovereign/v2/worker.py +199 -0
- sovereign/views/__init__.py +7 -0
- sovereign/views/api.py +82 -0
- sovereign/views/crypto.py +46 -15
- sovereign/views/discovery.py +55 -119
- sovereign/views/healthchecks.py +107 -20
- sovereign/views/interface.py +171 -111
- sovereign/worker.py +193 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0a4.dist-info}/METADATA +80 -76
- sovereign-1.0.0a4.dist-info/RECORD +85 -0
- {sovereign-0.19.3.dist-info → sovereign-1.0.0a4.dist-info}/WHEEL +1 -1
- sovereign-1.0.0a4.dist-info/entry_points.txt +46 -0
- sovereign_files/__init__.py +0 -0
- sovereign_files/static/darkmode.js +51 -0
- sovereign_files/static/node_expression.js +42 -0
- sovereign_files/static/panel.js +76 -0
- sovereign_files/static/resources.css +246 -0
- sovereign_files/static/resources.js +642 -0
- sovereign_files/static/sass/style.scss +33 -0
- sovereign_files/static/style.css +16143 -0
- sovereign_files/static/style.css.map +1 -0
- sovereign/config_loader.py +0 -225
- sovereign/discovery.py +0 -175
- sovereign/logs.py +0 -131
- sovereign/schemas.py +0 -780
- sovereign/sources/__init__.py +0 -3
- sovereign/sources/file.py +0 -21
- sovereign/sources/inline.py +0 -38
- sovereign/sources/lib.py +0 -40
- sovereign/sources/poller.py +0 -294
- sovereign/static/sass/style.scss +0 -27
- sovereign/static/style.css +0 -13553
- sovereign/templates/ul_filter.html +0 -22
- sovereign/utils/crypto.py +0 -103
- sovereign/views/admin.py +0 -120
- sovereign-0.19.3.dist-info/LICENSE.txt +0 -13
- sovereign-0.19.3.dist-info/RECORD +0 -47
- sovereign-0.19.3.dist-info/entry_points.txt +0 -10
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import pickle
|
|
2
|
+
import time
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from importlib.util import find_spec
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
from sovereign import application_logger as log
|
|
11
|
+
from sovereign.cache.backends import CacheBackend
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
# noinspection PyUnusedImports
|
|
15
|
+
import boto3
|
|
16
|
+
|
|
17
|
+
# noinspection PyUnusedImports
|
|
18
|
+
from botocore.exceptions import ClientError
|
|
19
|
+
except ImportError:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
BOTO_AVAILABLE = find_spec("boto3") is not None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class S3Client:
|
|
26
|
+
def __init__(self, role_arn: str | None, client_args: dict[str, Any]):
|
|
27
|
+
self.role_arn = role_arn
|
|
28
|
+
self.client_args = client_args
|
|
29
|
+
self._client = None
|
|
30
|
+
self._credentials_expiry = None
|
|
31
|
+
self._base_session = boto3.Session()
|
|
32
|
+
self._make_client()
|
|
33
|
+
|
|
34
|
+
def _make_client(self) -> None:
|
|
35
|
+
if self.role_arn:
|
|
36
|
+
log.debug(f"Refreshing credentials for role: {self.role_arn}")
|
|
37
|
+
sts = self._base_session.client("sts")
|
|
38
|
+
duration_seconds = 3600 # 4 hours
|
|
39
|
+
response = sts.assume_role(
|
|
40
|
+
RoleArn=self.role_arn,
|
|
41
|
+
RoleSessionName="sovereign-s3-cache",
|
|
42
|
+
DurationSeconds=duration_seconds,
|
|
43
|
+
)
|
|
44
|
+
credentials = response["Credentials"]
|
|
45
|
+
session = boto3.Session(
|
|
46
|
+
aws_access_key_id=credentials["AccessKeyId"],
|
|
47
|
+
aws_secret_access_key=credentials["SecretAccessKey"],
|
|
48
|
+
aws_session_token=credentials["SessionToken"],
|
|
49
|
+
)
|
|
50
|
+
self._credentials_expiry = credentials["Expiration"]
|
|
51
|
+
else:
|
|
52
|
+
session = self._base_session
|
|
53
|
+
self._credentials_expiry = None
|
|
54
|
+
self._client = session.client("s3", **self.client_args)
|
|
55
|
+
|
|
56
|
+
def _session_expiring_soon(self) -> bool:
|
|
57
|
+
if not self.role_arn or self._credentials_expiry is None:
|
|
58
|
+
return False
|
|
59
|
+
refresh_threshold = timedelta(minutes=30).seconds
|
|
60
|
+
time_until_expiry = (
|
|
61
|
+
self._credentials_expiry - datetime.now(timezone.utc)
|
|
62
|
+
).total_seconds()
|
|
63
|
+
return time_until_expiry <= refresh_threshold
|
|
64
|
+
|
|
65
|
+
def __getattr__(self, name):
|
|
66
|
+
if self._session_expiring_soon():
|
|
67
|
+
self._make_client()
|
|
68
|
+
return getattr(self._client, name)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class S3Backend(CacheBackend):
|
|
72
|
+
"""S3 cache backend implementation"""
|
|
73
|
+
|
|
74
|
+
# noinspection PyMissingConstructor,PyProtocol
|
|
75
|
+
@override
|
|
76
|
+
def __init__(self, config: dict[str, Any], attempts: int = 0) -> None: # pyright: ignore[reportMissingSuperCall]
|
|
77
|
+
"""Initialise S3 backend
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
config: Configuration dictionary containing S3 connection parameters
|
|
81
|
+
Expected keys: bucket_name, prefix
|
|
82
|
+
Optional keys: assume_role, endpoint_url
|
|
83
|
+
"""
|
|
84
|
+
if not BOTO_AVAILABLE:
|
|
85
|
+
raise ImportError("boto3 not installed")
|
|
86
|
+
|
|
87
|
+
self.bucket_name = config.get("bucket_name")
|
|
88
|
+
if not self.bucket_name:
|
|
89
|
+
raise ValueError("bucket_name is required for S3 cache backend")
|
|
90
|
+
|
|
91
|
+
self.prefix = config.get("prefix", "sovereign-cache")
|
|
92
|
+
self.registration_prefix = config.get("registration_prefix", "registrations-")
|
|
93
|
+
self.role = config.get("assume_role")
|
|
94
|
+
self.remote_cache_key_suffix = config.get("remote_cache_key_suffix")
|
|
95
|
+
|
|
96
|
+
client_args: dict[str, Any] = {}
|
|
97
|
+
if endpoint_url := config.get("endpoint_url"):
|
|
98
|
+
client_args["endpoint_url"] = endpoint_url
|
|
99
|
+
|
|
100
|
+
self.client_args = client_args
|
|
101
|
+
self.s3 = S3Client(self.role, self.client_args)
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
self.s3.head_bucket(Bucket=self.bucket_name)
|
|
105
|
+
log.info(f"S3 cache backend connected to bucket '{self.bucket_name}'")
|
|
106
|
+
except Exception as e:
|
|
107
|
+
if attempts == 0:
|
|
108
|
+
# wait 5 seconds and then try to access the bucket again
|
|
109
|
+
time.sleep(5)
|
|
110
|
+
self.__init__(config, attempts + 1)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
log.error(
|
|
114
|
+
f"Failed to access S3 bucket '{self.bucket_name}' with current credentials: {e}"
|
|
115
|
+
)
|
|
116
|
+
raise
|
|
117
|
+
|
|
118
|
+
def _make_key(self, key: str) -> str:
|
|
119
|
+
# noop
|
|
120
|
+
if self.remote_cache_key_suffix:
|
|
121
|
+
encoded_key = quote(f"{key}-{self.remote_cache_key_suffix}", safe="")
|
|
122
|
+
log.info(f"Using remote cache key: {encoded_key}")
|
|
123
|
+
else:
|
|
124
|
+
encoded_key = quote(key, safe="")
|
|
125
|
+
return f"{self.prefix}/{encoded_key}"
|
|
126
|
+
|
|
127
|
+
def get(self, key: str) -> Any | None:
|
|
128
|
+
try:
|
|
129
|
+
log.debug(f"Retrieving object {key} from bucket")
|
|
130
|
+
response = self.s3.get_object(
|
|
131
|
+
Bucket=self.bucket_name, Key=self._make_key(key)
|
|
132
|
+
)
|
|
133
|
+
data = response["Body"].read()
|
|
134
|
+
unpickled = pickle.loads(data)
|
|
135
|
+
log.debug(f"Successfully obtained object {key} from bucket")
|
|
136
|
+
return unpickled
|
|
137
|
+
except self.s3.exceptions.NoSuchKey:
|
|
138
|
+
log.debug(f"{key} not in bucket")
|
|
139
|
+
return None
|
|
140
|
+
except ClientError as e:
|
|
141
|
+
if e.response["Error"]["Code"] == "404":
|
|
142
|
+
log.debug(f"{key} not in bucket")
|
|
143
|
+
return None
|
|
144
|
+
log.warning(f"Failed to get key '{key}' from S3: {e}")
|
|
145
|
+
return None
|
|
146
|
+
except Exception as e:
|
|
147
|
+
log.warning(f"Failed to get key '{key}' from S3: {e}")
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
@override
|
|
151
|
+
def set(self, key: str, value: Any, timeout: int | None = None) -> None:
|
|
152
|
+
try:
|
|
153
|
+
log.debug(f"Putting new object {key} into bucket")
|
|
154
|
+
self.s3.put_object(
|
|
155
|
+
Bucket=self.bucket_name,
|
|
156
|
+
Key=self._make_key(key),
|
|
157
|
+
Body=pickle.dumps(value),
|
|
158
|
+
)
|
|
159
|
+
except Exception as e:
|
|
160
|
+
log.warning(f"Failed to set key '{key}' in S3: {e}")
|
|
161
|
+
raise
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sqlite3
|
|
3
|
+
from hashlib import sha256
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from cachelib import FileSystemCache
|
|
7
|
+
from typing_extensions import final
|
|
8
|
+
|
|
9
|
+
from sovereign.configuration import config
|
|
10
|
+
from sovereign.types import DiscoveryRequest
|
|
11
|
+
|
|
12
|
+
INIT = """
|
|
13
|
+
CREATE TABLE IF NOT EXISTS registered_clients (
|
|
14
|
+
client_id TEXT PRIMARY KEY,
|
|
15
|
+
discovery_request TEXT NOT NULL
|
|
16
|
+
)
|
|
17
|
+
"""
|
|
18
|
+
INSERT = "INSERT OR IGNORE INTO registered_clients (client_id, discovery_request) VALUES (?, ?)"
|
|
19
|
+
LIST = "SELECT client_id, discovery_request FROM registered_clients"
|
|
20
|
+
SEARCH = "SELECT 1 FROM registered_clients WHERE client_id = ?"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@final
|
|
24
|
+
class FilesystemCache:
|
|
25
|
+
def __init__(self, cache_path: str | None = None, default_timeout: int = 0):
|
|
26
|
+
self.cache_path = cache_path or config.cache.local_fs_path
|
|
27
|
+
self.default_timeout = default_timeout # 0 = infinite TTL
|
|
28
|
+
|
|
29
|
+
self._cache = FileSystemCache(
|
|
30
|
+
cache_dir=self.cache_path,
|
|
31
|
+
default_timeout=self.default_timeout,
|
|
32
|
+
hash_method=sha256,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Initialize SQLite for client registration
|
|
36
|
+
Path(self.cache_path).mkdir(parents=True, exist_ok=True)
|
|
37
|
+
self._db_path = Path(self.cache_path) / "clients.db"
|
|
38
|
+
self._init_db()
|
|
39
|
+
|
|
40
|
+
def _init_db(self):
|
|
41
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
42
|
+
_ = conn.execute(INIT)
|
|
43
|
+
|
|
44
|
+
def get(self, key):
|
|
45
|
+
return self._cache.get(key)
|
|
46
|
+
|
|
47
|
+
def set(self, key, value, timeout=None):
|
|
48
|
+
return self._cache.set(key, value, timeout)
|
|
49
|
+
|
|
50
|
+
def delete(self, key):
|
|
51
|
+
return self._cache.delete(key)
|
|
52
|
+
|
|
53
|
+
def clear(self):
|
|
54
|
+
return self._cache.clear()
|
|
55
|
+
|
|
56
|
+
def register(self, id: str, req: DiscoveryRequest) -> None:
|
|
57
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
58
|
+
_ = conn.execute(INSERT, (id, json.dumps(req.model_dump())))
|
|
59
|
+
|
|
60
|
+
def registered(self, id: str) -> bool:
|
|
61
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
62
|
+
cursor = conn.execute(SEARCH, (id,))
|
|
63
|
+
return cursor.fetchone() is not None
|
|
64
|
+
|
|
65
|
+
def get_registered_clients(self) -> list[tuple[str, DiscoveryRequest]]:
|
|
66
|
+
with sqlite3.connect(self._db_path) as conn:
|
|
67
|
+
cursor = conn.execute(LIST)
|
|
68
|
+
rows = cursor.fetchall()
|
|
69
|
+
|
|
70
|
+
result = []
|
|
71
|
+
for client_id, req_json in rows:
|
|
72
|
+
req = DiscoveryRequest.model_validate(json.loads(req_json))
|
|
73
|
+
result.append((client_id, req))
|
|
74
|
+
return result
|
sovereign/cache/types.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from sovereign.types import Node
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CacheResult(BaseModel):
|
|
9
|
+
value: Any
|
|
10
|
+
from_remote: bool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Entry(BaseModel):
|
|
14
|
+
text: str
|
|
15
|
+
len: int
|
|
16
|
+
version: str
|
|
17
|
+
node: Node
|