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 +2 -0
- sovereign/sources/poller.py +66 -73
- sovereign/statistics.py +6 -4
- sovereign/views/api.py +2 -3
- {sovereign-1.0.0b103.dist-info → sovereign-1.0.0b104.dist-info}/METADATA +1 -1
- {sovereign-1.0.0b103.dist-info → sovereign-1.0.0b104.dist-info}/RECORD +9 -9
- {sovereign-1.0.0b103.dist-info → sovereign-1.0.0b104.dist-info}/LICENSE.txt +0 -0
- {sovereign-1.0.0b103.dist-info → sovereign-1.0.0b104.dist-info}/WHEEL +0 -0
- {sovereign-1.0.0b103.dist-info → sovereign-1.0.0b104.dist-info}/entry_points.txt +0 -0
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",
|
sovereign/sources/poller.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
57
|
-
super()._report(
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
41
|
-
for expr in expand_metadata_to_expr(
|
|
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")
|
|
@@ -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=
|
|
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=
|
|
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
|
|
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=
|
|
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.
|
|
64
|
-
sovereign-1.0.
|
|
65
|
-
sovereign-1.0.
|
|
66
|
-
sovereign-1.0.
|
|
67
|
-
sovereign-1.0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|