sovereign 1.0.0b103__py3-none-any.whl → 1.0.0b104__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/schemas.py CHANGED
@@ -716,6 +716,8 @@ class ContextConfiguration(BaseSettings):
716
716
 
717
717
  class SourcesConfiguration(BaseSettings):
718
718
  refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
719
+ max_retries: int = Field(3, alias="SOVEREIGN_SOURCES_MAX_RETRIES")
720
+ retry_delay: int = Field(1, alias="SOVEREIGN_SOURCES_RETRY_DELAY")
719
721
  cache_strategy: Optional[Any] = None
720
722
  model_config = SettingsConfigDict(
721
723
  env_file=".env",
@@ -36,80 +36,57 @@ GMods = Dict[str, Type[GlobalModifier]]
36
36
 
37
37
  def _deep_diff(old, new, path="") -> list[dict[str, Any]]:
38
38
  changes = []
39
-
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):
@@ -202,7 +165,7 @@ def source_diff_summary(prev, curr) -> dict[str, Any]:
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)
sovereign/statistics.py CHANGED
@@ -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()
sovereign/views/api.py CHANGED
@@ -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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 1.0.0b103
3
+ Version: 1.0.0b104
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
@@ -17,18 +17,18 @@ sovereign/modifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSu
17
17
  sovereign/modifiers/lib.py,sha256=Cx0VrpTKbSjb3YmHyG4Jy6YEaPlrwpeqNaom3zu1_hw,2885
18
18
  sovereign/rendering.py,sha256=MIA7se7-C4WTWf7xZSgqpf7NvhDT7NkZbR3_G9N1dHI,5015
19
19
  sovereign/response_class.py,sha256=beMAFV-4L6DwyWzJzy71GkEW4gb7fzH1jd8-Tul13cU,427
20
- sovereign/schemas.py,sha256=RtZm1qVIuXF9M6FXHa0VP6C0lBNU6sDlmM5k28RJXxM,37271
20
+ sovereign/schemas.py,sha256=PlAhNOeVKLlKIlMHKAHNfkPiE1cnpRnIIiNgJVr5ChQ,37413
21
21
  sovereign/server.py,sha256=3qfcUGaRrTF2GTfGEJgGnPXBiZGis1qxOlpIKvoCyFA,2596
22
22
  sovereign/sources/__init__.py,sha256=g9hEpFk8j5i1ApHQpbc9giTyJW41Ppgsqv5P9zGxOJk,78
23
23
  sovereign/sources/file.py,sha256=prUThsDCSPNwZaZpkKXhAm-GVRZWbBoGKGU0It4HHXs,690
24
24
  sovereign/sources/inline.py,sha256=pP77m7bHjqE3sSoqZthcuw1ARVMf9gooVwbz4B8OAek,1003
25
25
  sovereign/sources/lib.py,sha256=0hk_G6mKJrB65WokVZnqF5kdJ3vsQZMNPuJqJO0mBsI,1031
26
- sovereign/sources/poller.py,sha256=ahR3C08BspaoQxT2-IaJckAQf9uZgyY3w5J5SunFtoI,17824
26
+ sovereign/sources/poller.py,sha256=9iwmP4wjc951OTxKDB8RYhKwEKpgW5tC5-Js5OA6zVo,18415
27
27
  sovereign/static/node_expression.js,sha256=dL9QLM49jorqavf3Qtye6E1QTWYDT1rFI0tQR1HsiLQ,504
28
28
  sovereign/static/panel.js,sha256=i5mGExjv-I4Gtt9dQiTyFwPZa8pg5rXeuTeidXNUiTE,2695
29
29
  sovereign/static/sass/style.scss,sha256=tPHPEm3sZeBFGDyyn3pHcA-nbaKT-h-UsSTsf6dHNDU,1158
30
30
  sovereign/static/style.css,sha256=vG8HPsbCbPIZfHgy7gSeof97Pnp0okkyaXyJzIEEW-8,447517
31
- sovereign/statistics.py,sha256=gXpQgQOTKqU68loQ_NU1OmxNvsRpAp38RpunjbecIRo,2587
31
+ sovereign/statistics.py,sha256=-me83Bkfya5vQai3Irrf5R-RNG-XE7wlyqQFqKeueFo,2666
32
32
  sovereign/templates/base.html,sha256=5vw3-NmN291pXRdArpCwhSce9bAYBWCJVRhvO5EmE9g,2296
33
33
  sovereign/templates/err.html,sha256=a3cEzOqyqWOIe3YxfTEjkxbTfxBxq1knD6GwzEFljfs,603
34
34
  sovereign/templates/resources.html,sha256=QaZ1S38JhAZg3-PfQS1cAKhCczVLXw9e4pztBrqr4qs,40217
@@ -54,14 +54,14 @@ sovereign/utils/timer.py,sha256=_dUtEasj0BKbWYuQ_T3HFIyjurXXj-La-dNSMAwKMSo,795
54
54
  sovereign/utils/version_info.py,sha256=adBfu0z6jsg8E5-BIUjZyBwZvfLASj7fpCpYeIvBeMY,576
55
55
  sovereign/utils/weighted_clusters.py,sha256=bPzuRE7Qgvv04HcR2AhMDvBrFlZ8AfteweLKhY9SvWg,1166
56
56
  sovereign/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
- sovereign/views/api.py,sha256=T_le6Cfw03KsAc1nx93vZw97uUIFtF86vz9daXR_yUM,2177
57
+ sovereign/views/api.py,sha256=jKVjSvi0Tr1HHix3hc0H8yGZoyDind2sJ4w7a4pJvy0,2168
58
58
  sovereign/views/crypto.py,sha256=7y0eHWtt-bbr2CwHEkH7odPaJ1IEviU-71U-MYJD0Kc,3360
59
59
  sovereign/views/discovery.py,sha256=4sVRPWpH8nYwY26GWK9N8maOYUNbIJTrUckZ3MahqAU,1876
60
60
  sovereign/views/healthchecks.py,sha256=TaXbxkX679jyQ8v5FxtBa2Qa0Z7KuqQ10WgAqfuVGUc,1743
61
61
  sovereign/views/interface.py,sha256=FmQ7LiUPLSvkEDOKCncrnKMD9g1lJKu-DQNbbyi8mqk,6346
62
62
  sovereign/worker.py,sha256=NqXlfi9QRlXcWeUPqHWvnYx2CPldY44iLb_fBJAiZ-4,4983
63
- sovereign-1.0.0b103.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
64
- sovereign-1.0.0b103.dist-info/METADATA,sha256=KGa6EiG_oC1w-LS3IfL1yzDGxCVVwNp-WbrqdHr6pHg,6268
65
- sovereign-1.0.0b103.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
- sovereign-1.0.0b103.dist-info/entry_points.txt,sha256=VKJdnnN_HNL8xYQMXsFXfFmN6QkdXMEk5S964avxQJI,1404
67
- sovereign-1.0.0b103.dist-info/RECORD,,
63
+ sovereign-1.0.0b104.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
64
+ sovereign-1.0.0b104.dist-info/METADATA,sha256=qjbGaqLJwlJsHaJuKbaye4TL1JRudbtuF0mVIN6sPqg,6268
65
+ sovereign-1.0.0b104.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
+ sovereign-1.0.0b104.dist-info/entry_points.txt,sha256=VKJdnnN_HNL8xYQMXsFXfFmN6QkdXMEk5S964avxQJI,1404
67
+ sovereign-1.0.0b104.dist-info/RECORD,,