sovereign 1.0.0b103__tar.gz → 1.0.0b106__tar.gz

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 (66) hide show
  1. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/PKG-INFO +1 -1
  2. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/pyproject.toml +1 -1
  3. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/schemas.py +19 -1
  4. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/server.py +7 -3
  5. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/sources/poller.py +69 -76
  6. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/statistics.py +6 -4
  7. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/views/api.py +2 -3
  8. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/LICENSE.txt +0 -0
  9. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/README.md +0 -0
  10. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/__init__.py +0 -0
  11. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/app.py +0 -0
  12. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/cache.py +0 -0
  13. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/constants.py +0 -0
  14. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/context.py +0 -0
  15. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/dynamic_config/__init__.py +0 -0
  16. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/dynamic_config/deser.py +0 -0
  17. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/dynamic_config/loaders.py +0 -0
  18. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/error_info.py +0 -0
  19. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/logging/access_logger.py +0 -0
  20. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/logging/application_logger.py +0 -0
  21. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/logging/base_logger.py +0 -0
  22. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/logging/bootstrapper.py +0 -0
  23. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/logging/types.py +0 -0
  24. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/middlewares.py +0 -0
  25. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/modifiers/__init__.py +0 -0
  26. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/modifiers/lib.py +0 -0
  27. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/rendering.py +0 -0
  28. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/response_class.py +0 -0
  29. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/sources/__init__.py +0 -0
  30. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/sources/file.py +0 -0
  31. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/sources/inline.py +0 -0
  32. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/sources/lib.py +0 -0
  33. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/static/node_expression.js +0 -0
  34. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/static/panel.js +0 -0
  35. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/static/sass/style.scss +0 -0
  36. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/static/style.css +0 -0
  37. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/templates/base.html +0 -0
  38. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/templates/err.html +0 -0
  39. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/templates/resources.html +0 -0
  40. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/testing/loaders.py +0 -0
  41. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/testing/modifiers.py +0 -0
  42. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/tracing.py +0 -0
  43. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/__init__.py +0 -0
  44. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/auth.py +0 -0
  45. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/crypto/__init__.py +0 -0
  46. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/crypto/crypto.py +0 -0
  47. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/crypto/suites/__init__.py +0 -0
  48. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +0 -0
  49. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/crypto/suites/base_cipher.py +0 -0
  50. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/crypto/suites/disabled_cipher.py +0 -0
  51. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/crypto/suites/fernet_cipher.py +0 -0
  52. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/dictupdate.py +0 -0
  53. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/eds.py +0 -0
  54. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/entry_point_loader.py +0 -0
  55. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/mock.py +0 -0
  56. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/resources.py +0 -0
  57. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/templates.py +0 -0
  58. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/timer.py +0 -0
  59. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/version_info.py +0 -0
  60. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/utils/weighted_clusters.py +0 -0
  61. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/views/__init__.py +0 -0
  62. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/views/crypto.py +0 -0
  63. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/views/discovery.py +0 -0
  64. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/views/healthchecks.py +0 -0
  65. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/views/interface.py +0 -0
  66. {sovereign-1.0.0b103 → sovereign-1.0.0b106}/src/sovereign/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 1.0.0b103
3
+ Version: 1.0.0b106
4
4
  Summary: Envoy Proxy control-plane written in Python
5
5
  Home-page: https://pypi.org/project/sovereign/
6
6
  License: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sovereign"
3
- version = "1.0.0b103"
3
+ version = "1.0.0b106"
4
4
  description = "Envoy Proxy control-plane written in Python"
5
5
  license = "Apache-2.0"
6
6
  packages = [
@@ -6,7 +6,6 @@ from pathlib import Path
6
6
  from enum import Enum
7
7
  from os import getenv
8
8
  from types import ModuleType
9
- from collections import defaultdict
10
9
  from dataclasses import dataclass
11
10
  from functools import cached_property
12
11
  from typing import Any, Dict, List, Mapping, Optional, Self, Tuple, Union, Callable
@@ -716,6 +715,8 @@ class ContextConfiguration(BaseSettings):
716
715
 
717
716
  class SourcesConfiguration(BaseSettings):
718
717
  refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
718
+ max_retries: int = Field(3, alias="SOVEREIGN_SOURCES_MAX_RETRIES")
719
+ retry_delay: int = Field(1, alias="SOVEREIGN_SOURCES_RETRY_DELAY")
719
720
  cache_strategy: Optional[Any] = None
720
721
  model_config = SettingsConfigDict(
721
722
  env_file=".env",
@@ -765,6 +766,20 @@ class TracingConfig(BaseSettings):
765
766
  return values
766
767
 
767
768
 
769
+ class SupervisordConfig(BaseSettings):
770
+ nodaemon: bool = Field(True, alias="SOVEREIGN_SUPERVISORD_NODAEMON")
771
+ loglevel: str = Field("error", alias="SOVEREIGN_SUPERVISORD_LOGLEVEL")
772
+ pidfile: str = Field("/tmp/supervisord.pid", alias="SOVEREIGN_SUPERVISORD_PIDFILE")
773
+ logfile: str = Field("/tmp/supervisord.log", alias="SOVEREIGN_SUPERVISORD_LOGFILE")
774
+ directory: str = Field("%(here)s", alias="SOVEREIGN_SUPERVISORD_DIRECTORY")
775
+ model_config = SettingsConfigDict(
776
+ env_file=".env",
777
+ extra="ignore",
778
+ env_file_encoding="utf-8",
779
+ populate_by_name=True,
780
+ )
781
+
782
+
768
783
  class LegacyConfig(BaseSettings):
769
784
  regions: Optional[List[str]] = None
770
785
  eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
@@ -856,6 +871,9 @@ class SovereignConfigv2(BaseSettings):
856
871
  tracing: Optional[TracingConfig] = Field(default_factory=TracingConfig)
857
872
  debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
858
873
 
874
+ # Supervisord settings
875
+ supervisord: SupervisordConfig = SupervisordConfig()
876
+
859
877
  # Deprecated in 0.30
860
878
  sources: Optional[List[ConfiguredSource]] = Field(None, deprecated=True)
861
879
  source_config: SourcesConfiguration = Field(
@@ -8,10 +8,11 @@ import uvicorn
8
8
  from sovereign import application_logger as log
9
9
  from sovereign.app import app
10
10
  from sovereign.worker import worker as worker_app
11
- from sovereign.schemas import SovereignAsgiConfig
11
+ from sovereign.schemas import SovereignAsgiConfig, SupervisordConfig
12
12
 
13
13
 
14
14
  asgi_config = SovereignAsgiConfig()
15
+ supervisord_config = SupervisordConfig()
15
16
 
16
17
 
17
18
  def web() -> None:
@@ -59,8 +60,11 @@ def write_supervisor_conf() -> Path:
59
60
 
60
61
  conf = configparser.RawConfigParser()
61
62
  conf["supervisord"] = supervisord = {
62
- "nodaemon": "true",
63
- "loglevel": "error",
63
+ "nodaemon": str(supervisord_config.nodaemon).lower(),
64
+ "loglevel": supervisord_config.loglevel,
65
+ "pidfile": supervisord_config.pidfile,
66
+ "logfile": supervisord_config.logfile,
67
+ "directory": supervisord_config.directory,
64
68
  }
65
69
 
66
70
  conf["fcgi-program:web"] = web = {
@@ -35,81 +35,58 @@ GMods = Dict[str, Type[GlobalModifier]]
35
35
 
36
36
 
37
37
  def _deep_diff(old, new, path="") -> list[dict[str, Any]]:
38
- changes = []
39
-
38
+ changes: list[dict[str, Any]] = []
39
+
40
40
  # handle add/remove
41
41
  if (old, new) == (None, None):
42
42
  return changes
43
43
  elif old is None:
44
- changes.append({
45
- "op": "add",
46
- "path": path,
47
- "value": new
48
- })
44
+ changes.append({"op": "add", "path": path, "value": new})
49
45
  return changes
50
46
  elif new is None:
51
- changes.append({
52
- "op": "remove",
53
- "path": path,
54
- "old_value": old
55
- })
47
+ changes.append({"op": "remove", "path": path, "old_value": old})
56
48
  return changes
57
-
49
+
58
50
  # handle completely different types
59
51
  if type(old) is not type(new):
60
- changes.append({
61
- "op": "change",
62
- "path": path,
63
- "old_value": old,
64
- "new_value": new
65
- })
52
+ changes.append(
53
+ {"op": "change", "path": path, "old_value": old, "new_value": new}
54
+ )
66
55
  return changes
67
-
56
+
68
57
  # handle fields recursively
69
58
  if isinstance(old, dict) and isinstance(new, dict):
70
59
  all_keys = set(old.keys()) | set(new.keys())
71
-
60
+
72
61
  for key in sorted(all_keys):
73
62
  old_val = old.get(key)
74
63
  new_val = new.get(key)
75
-
64
+
76
65
  current_path = f"{path}.{key}" if path else key
77
-
66
+
78
67
  if key not in old:
79
- changes.append({
80
- "op": "add",
81
- "path": current_path,
82
- "value": new_val
83
- })
68
+ changes.append({"op": "add", "path": current_path, "value": new_val})
84
69
  elif key not in new:
85
- changes.append({
86
- "op": "remove",
87
- "path": current_path,
88
- "old_value": old_val
89
- })
70
+ changes.append(
71
+ {"op": "remove", "path": current_path, "old_value": old_val}
72
+ )
90
73
  elif old_val != new_val:
91
74
  nested_changes = _deep_diff(old_val, new_val, current_path)
92
75
  changes.extend(nested_changes)
93
-
76
+
94
77
  # handle items recursively
95
78
  elif isinstance(old, list) and isinstance(new, list):
96
79
  max_len = max(len(old), len(new))
97
-
80
+
98
81
  for i in range(max_len):
99
82
  current_path = f"{path}[{i}]" if path else f"[{i}]"
100
-
83
+
101
84
  if i >= len(old):
102
- changes.append({
103
- "op": "add",
104
- "path": current_path,
105
- "value": new[i]
106
- })
85
+ changes.append({"op": "add", "path": current_path, "value": new[i]})
107
86
  elif i >= len(new):
108
- changes.append({
109
- "op": "remove",
110
- "path": current_path,
111
- "old_value": old[i]
112
- })
87
+ changes.append(
88
+ {"op": "remove", "path": current_path, "old_value": old[i]}
89
+ )
113
90
  elif old[i] != new[i]:
114
91
  nested_changes = _deep_diff(old[i], new[i], current_path)
115
92
  changes.extend(nested_changes)
@@ -117,13 +94,10 @@ def _deep_diff(old, new, path="") -> list[dict[str, Any]]:
117
94
  # handle primitives
118
95
  else:
119
96
  if old != new:
120
- changes.append({
121
- "op": "change",
122
- "path": path,
123
- "old_value": old,
124
- "new_value": new
125
- })
126
-
97
+ changes.append(
98
+ {"op": "change", "path": path, "old_value": old, "new_value": new}
99
+ )
100
+
127
101
  return changes
128
102
 
129
103
 
@@ -136,17 +110,9 @@ def per_field_diff(old, new) -> list[dict[str, Any]]:
136
110
  new_inst = new[i] if i < len(new) else None
137
111
 
138
112
  if old_inst is None:
139
- changes.append({
140
- "op": "add",
141
- "path": f"[{i}]",
142
- "value": new_inst
143
- })
113
+ changes.append({"op": "add", "path": f"[{i}]", "value": new_inst})
144
114
  elif new_inst is None:
145
- changes.append({
146
- "op": "remove",
147
- "path": f"[{i}]",
148
- "old_value": old_inst
149
- })
115
+ changes.append({"op": "remove", "path": f"[{i}]", "old_value": old_inst})
150
116
  elif old_inst != new_inst:
151
117
  # Use the deep diff with index prefix
152
118
  field_changes = _deep_diff(old_inst, new_inst, f"[{i}]")
@@ -156,7 +122,7 @@ def per_field_diff(old, new) -> list[dict[str, Any]]:
156
122
 
157
123
 
158
124
  def _gen_uuid(diff_summary: dict[str, Any]) -> str:
159
- blob = json.dumps(diff_summary, sort_keys=True, separators=('', ''))
125
+ blob = json.dumps(diff_summary, sort_keys=True, separators=("", ""))
160
126
  return str(uuid.uuid5(uuid.NAMESPACE_DNS, blob))
161
127
 
162
128
 
@@ -168,14 +134,11 @@ def source_diff_summary(prev, curr) -> dict[str, Any]:
168
134
  scope: {"added": len(instances)}
169
135
  for scope, instances in curr.scopes.items()
170
136
  if instances
171
- }
137
+ },
172
138
  }
173
139
  else:
174
- summary = {
175
- "type": "update",
176
- "scopes": {}
177
- }
178
-
140
+ summary = {"type": "update", "scopes": {}}
141
+
179
142
  all_scopes = set(prev.scopes.keys()) | set(curr.scopes.keys())
180
143
 
181
144
  for scope in sorted(all_scopes):
@@ -185,7 +148,7 @@ def source_diff_summary(prev, curr) -> dict[str, Any]:
185
148
  n_old = len(old)
186
149
  n_new = len(new)
187
150
 
188
- scope_changes = {}
151
+ scope_changes: dict[str, Any] = {}
189
152
 
190
153
  if n_old == 0 and n_new > 0:
191
154
  scope_changes["added"] = n_new
@@ -198,11 +161,11 @@ def source_diff_summary(prev, curr) -> dict[str, Any]:
198
161
  scope_changes["count_change"] = n_new - n_old
199
162
 
200
163
  if scope_changes:
201
- summary["scopes"][scope] = scope_changes
164
+ summary["scopes"][scope] = scope_changes # type: ignore
202
165
 
203
166
  if not summary["scopes"]:
204
167
  summary = {"type": "no_changes"}
205
-
168
+
206
169
  summary["uuid"] = _gen_uuid(summary)
207
170
  return summary
208
171
 
@@ -239,13 +202,17 @@ class SourcePoller:
239
202
  self.global_modifiers: GMods = dict()
240
203
 
241
204
  # initially set data and modify
242
- self.source_data: SourceData
205
+ self.source_data: SourceData = SourceData()
206
+ self.source_data_modified: SourceData = SourceData()
243
207
  self.last_updated = datetime.now()
244
208
  self.instance_count = 0
245
209
 
246
210
  self.cache: dict[str, dict[str, list[dict[str, Any]]]] = {}
247
211
  self.registry: set[Any] = set()
248
212
 
213
+ # Retry state
214
+ self.retry_count = 0
215
+
249
216
  @property
250
217
  def data_is_stale(self) -> bool:
251
218
  return self.last_updated < datetime.now() - timedelta(minutes=2)
@@ -328,6 +295,10 @@ class SourcePoller:
328
295
 
329
296
  def refresh(self) -> bool:
330
297
  self.stats.increment("sources.attempt")
298
+
299
+ # Get retry config from global source config
300
+ max_retries = config.source_config.max_retries
301
+
331
302
  try:
332
303
  new = SourceData()
333
304
  for source in self.sources:
@@ -336,15 +307,25 @@ class SourcePoller:
336
307
  new.scopes[scope] = []
337
308
  new.scopes[scope].extend(source.get())
338
309
  except Exception as e:
310
+ self.retry_count += 1
339
311
  self.logger.error(
340
- event="Error while refreshing sources",
312
+ event=f"Error while refreshing sources (attempt {self.retry_count}/{max_retries})",
341
313
  traceback=[line for line in traceback.format_exc().split("\n")],
342
314
  error=e.__class__.__name__,
343
315
  detail=getattr(e, "detail", "-"),
316
+ retry_count=self.retry_count,
344
317
  )
345
318
  self.stats.increment("sources.error")
319
+
320
+ if self.retry_count >= max_retries:
321
+ # Reset retry count for next cycle
322
+ self.retry_count = 0
323
+ self.stats.increment("sources.error.final")
346
324
  return False
347
325
 
326
+ # Success - reset retry count
327
+ self.retry_count = 0
328
+
348
329
  # Is the new data the same as what we currently have
349
330
  if new == getattr(self, "source_data", None):
350
331
  self.stats.increment("sources.unchanged")
@@ -513,5 +494,17 @@ class SourcePoller:
513
494
  while True:
514
495
  try:
515
496
  self.poll()
516
- finally:
497
+
498
+ # If we have retry count, use exponential backoff for next attempt
499
+ if self.retry_count > 0:
500
+ retry_delay = config.source_config.retry_delay
501
+ delay = min(
502
+ retry_delay * (2 ** (self.retry_count - 1)),
503
+ self.source_refresh_rate, # Cap at normal refresh rate
504
+ )
505
+ await asyncio.sleep(delay)
506
+ else:
507
+ await asyncio.sleep(self.source_refresh_rate)
508
+ except Exception as e:
509
+ self.logger.error(f"Unexpected error in poll loop: {e}")
517
510
  await asyncio.sleep(self.source_refresh_rate)
@@ -53,10 +53,12 @@ def configure_statsd() -> StatsDProxy:
53
53
  from datadog import DogStatsd
54
54
 
55
55
  class CustomStatsd(DogStatsd): # type: ignore
56
- def _report(self, metric, metric_type, value, tags, sample_rate) -> None: # type: ignore
57
- super()._report(metric, metric_type, value, tags, sample_rate)
58
- self.emitted: Dict[str, Any] = dict()
59
- self.emitted[metric] = self.emitted.setdefault(metric, 0) + 1
56
+ def _report(self, *args, **kwargs) -> None: # type: ignore
57
+ super()._report(*args, **kwargs)
58
+ # Capture the metric name and increment its count for debugging
59
+ if metric := kwargs.get("metric"):
60
+ self.emitted: Dict[str, Any] = dict()
61
+ self.emitted[metric] = self.emitted.setdefault(metric, 0) + 1
60
62
 
61
63
  module: Optional[CustomStatsd]
62
64
  module = CustomStatsd()
@@ -37,8 +37,8 @@ async def resource(
37
37
  ) -> Response:
38
38
  expressions = [f"cluster={service_cluster}"]
39
39
  try:
40
- metadata = json.loads(metadata or "{}")
41
- for expr in expand_metadata_to_expr(metadata):
40
+ data = {"metadata": json.loads(metadata or "{}")}
41
+ for expr in expand_metadata_to_expr(data):
42
42
  expressions.append(expr)
43
43
  except Exception:
44
44
  pass
@@ -51,7 +51,6 @@ async def resource(
51
51
  expressions=expressions,
52
52
  )
53
53
  req = mock_discovery_request(**{k: v for k, v in kwargs.items() if v is not None})
54
- print(req)
55
54
  response = await cache.blocking_read(req)
56
55
  if content := getattr(response, "text", None):
57
56
  return Response(content, media_type="application/json")
File without changes
File without changes