sovereign 0.14.2__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.

Files changed (99) hide show
  1. sovereign/__init__.py +17 -78
  2. sovereign/app.py +74 -59
  3. sovereign/cache/__init__.py +245 -0
  4. sovereign/cache/backends/__init__.py +110 -0
  5. sovereign/cache/backends/s3.py +161 -0
  6. sovereign/cache/filesystem.py +74 -0
  7. sovereign/cache/types.py +17 -0
  8. sovereign/configuration.py +607 -0
  9. sovereign/constants.py +1 -0
  10. sovereign/context.py +271 -100
  11. sovereign/dynamic_config/__init__.py +112 -0
  12. sovereign/dynamic_config/deser.py +78 -0
  13. sovereign/dynamic_config/loaders.py +120 -0
  14. sovereign/error_info.py +61 -0
  15. sovereign/events.py +49 -0
  16. sovereign/logging/access_logger.py +85 -0
  17. sovereign/logging/application_logger.py +54 -0
  18. sovereign/logging/base_logger.py +41 -0
  19. sovereign/logging/bootstrapper.py +36 -0
  20. sovereign/logging/types.py +10 -0
  21. sovereign/middlewares.py +8 -7
  22. sovereign/modifiers/lib.py +2 -1
  23. sovereign/rendering.py +124 -0
  24. sovereign/rendering_common.py +91 -0
  25. sovereign/response_class.py +18 -0
  26. sovereign/server.py +123 -28
  27. sovereign/statistics.py +19 -21
  28. sovereign/templates/base.html +59 -46
  29. sovereign/templates/resources.html +203 -102
  30. sovereign/testing/loaders.py +9 -0
  31. sovereign/{modifiers/test.py → testing/modifiers.py} +0 -2
  32. sovereign/tracing.py +103 -0
  33. sovereign/types.py +304 -0
  34. sovereign/utils/auth.py +27 -13
  35. sovereign/utils/crypto/__init__.py +0 -0
  36. sovereign/utils/crypto/crypto.py +135 -0
  37. sovereign/utils/crypto/suites/__init__.py +21 -0
  38. sovereign/utils/crypto/suites/aes_gcm_cipher.py +42 -0
  39. sovereign/utils/crypto/suites/base_cipher.py +21 -0
  40. sovereign/utils/crypto/suites/disabled_cipher.py +25 -0
  41. sovereign/utils/crypto/suites/fernet_cipher.py +29 -0
  42. sovereign/utils/dictupdate.py +3 -2
  43. sovereign/utils/eds.py +40 -22
  44. sovereign/utils/entry_point_loader.py +18 -0
  45. sovereign/utils/mock.py +60 -17
  46. sovereign/utils/resources.py +17 -0
  47. sovereign/utils/templates.py +4 -2
  48. sovereign/utils/timer.py +5 -3
  49. sovereign/utils/version_info.py +8 -0
  50. sovereign/utils/weighted_clusters.py +2 -1
  51. sovereign/v2/__init__.py +0 -0
  52. sovereign/v2/data/data_store.py +621 -0
  53. sovereign/v2/data/render_discovery_response.py +24 -0
  54. sovereign/v2/data/repositories.py +90 -0
  55. sovereign/v2/data/utils.py +33 -0
  56. sovereign/v2/data/worker_queue.py +273 -0
  57. sovereign/v2/jobs/refresh_context.py +117 -0
  58. sovereign/v2/jobs/render_discovery_job.py +145 -0
  59. sovereign/v2/logging.py +81 -0
  60. sovereign/v2/types.py +41 -0
  61. sovereign/v2/web.py +101 -0
  62. sovereign/v2/worker.py +199 -0
  63. sovereign/views/__init__.py +7 -0
  64. sovereign/views/api.py +82 -0
  65. sovereign/views/crypto.py +46 -15
  66. sovereign/views/discovery.py +52 -67
  67. sovereign/views/healthchecks.py +107 -20
  68. sovereign/views/interface.py +173 -117
  69. sovereign/worker.py +193 -0
  70. {sovereign-0.14.2.dist-info → sovereign-1.0.0a4.dist-info}/METADATA +81 -73
  71. sovereign-1.0.0a4.dist-info/RECORD +85 -0
  72. {sovereign-0.14.2.dist-info → sovereign-1.0.0a4.dist-info}/WHEEL +1 -1
  73. sovereign-1.0.0a4.dist-info/entry_points.txt +46 -0
  74. sovereign_files/__init__.py +0 -0
  75. sovereign_files/static/darkmode.js +51 -0
  76. sovereign_files/static/node_expression.js +42 -0
  77. sovereign_files/static/panel.js +76 -0
  78. sovereign_files/static/resources.css +246 -0
  79. sovereign_files/static/resources.js +642 -0
  80. sovereign_files/static/sass/style.scss +33 -0
  81. sovereign_files/static/style.css +16143 -0
  82. sovereign_files/static/style.css.map +1 -0
  83. sovereign/config_loader.py +0 -225
  84. sovereign/discovery.py +0 -175
  85. sovereign/logs.py +0 -131
  86. sovereign/schemas.py +0 -715
  87. sovereign/sources/__init__.py +0 -3
  88. sovereign/sources/file.py +0 -21
  89. sovereign/sources/inline.py +0 -38
  90. sovereign/sources/lib.py +0 -40
  91. sovereign/sources/poller.py +0 -298
  92. sovereign/static/sass/style.scss +0 -27
  93. sovereign/static/style.css +0 -13553
  94. sovereign/templates/ul_filter.html +0 -22
  95. sovereign/utils/crypto.py +0 -64
  96. sovereign/views/admin.py +0 -120
  97. sovereign-0.14.2.dist-info/LICENSE.txt +0 -13
  98. sovereign-0.14.2.dist-info/RECORD +0 -45
  99. sovereign-0.14.2.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
@@ -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