sovereign 0.27.1__py3-none-any.whl → 0.29.0__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 CHANGED
@@ -7,7 +7,7 @@ from fastapi.responses import JSONResponse
7
7
  from pydantic import ValidationError
8
8
  from starlette.templating import Jinja2Templates
9
9
 
10
- from sovereign import config_loader
10
+ from sovereign import dynamic_config
11
11
  from sovereign.context import TemplateContext
12
12
  from sovereign.logging.bootstrapper import LoggerBootstrapper
13
13
  from sovereign.schemas import SovereignAsgiConfig, SovereignConfig, SovereignConfigv2
@@ -36,7 +36,7 @@ except ImportError:
36
36
  def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
37
37
  ret: Mapping[Any, Any] = dict()
38
38
  for p in path.split(","):
39
- spec = config_loader.Loadable.from_legacy_fmt(p)
39
+ spec = dynamic_config.Loadable.from_legacy_fmt(p)
40
40
  ret = merge(obj_a=ret, obj_b=spec.load(), merge_lists=True)
41
41
  return ret
42
42
 
@@ -3,7 +3,7 @@ from typing import Any, Mapping
3
3
 
4
4
  from pydantic import ValidationError
5
5
 
6
- from sovereign import config_loader
6
+ from sovereign import dynamic_config
7
7
  from sovereign.context import TemplateContext
8
8
  from sovereign.logging.bootstrapper import LoggerBootstrapper
9
9
  from sovereign.schemas import SovereignAsgiConfig, SovereignConfig, SovereignConfigv2
@@ -16,7 +16,7 @@ from sovereign.utils.dictupdate import merge # type: ignore
16
16
  def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
17
17
  ret: Mapping[Any, Any] = dict()
18
18
  for p in path.split(","):
19
- spec = config_loader.Loadable.from_legacy_fmt(p)
19
+ spec = dynamic_config.Loadable.from_legacy_fmt(p)
20
20
  ret = merge(obj_a=ret, obj_b=spec.load(), merge_lists=True)
21
21
  return ret
22
22
 
sovereign/context.py CHANGED
@@ -13,11 +13,9 @@ from typing import (
13
13
  )
14
14
 
15
15
  from fastapi import HTTPException
16
- from sovereign import config_loader
17
- from sovereign.utils.entry_point_loader import EntryPointLoader
18
16
  from structlog.stdlib import BoundLogger
19
17
 
20
- from sovereign.config_loader import Loadable
18
+ from sovereign.dynamic_config import Loadable
21
19
  from sovereign.schemas import DiscoveryRequest, EncryptionConfig, XdsTemplate
22
20
  from sovereign.sources import SourcePoller
23
21
  from sovereign.utils.crypto.crypto import CipherContainer
@@ -55,14 +53,6 @@ class TemplateContext:
55
53
  self.stats = stats
56
54
  # initial load
57
55
  self.context: Dict[str, Any] = {}
58
- entry_points = EntryPointLoader("loaders")
59
- for entry_point in entry_points.groups["loaders"]:
60
- custom_loader = entry_point.load()
61
- try:
62
- func = custom_loader.load
63
- except AttributeError:
64
- raise AttributeError("Custom loader does not implement .load()")
65
- config_loader.loaders[entry_point.name] = func
66
56
  asyncio.run(self.load_context_variables())
67
57
 
68
58
  async def start_refresh_context(self) -> NoReturn:
sovereign/discovery.py CHANGED
@@ -163,6 +163,8 @@ def filter_resources(
163
163
  that does not match the name of the resource.
164
164
  If Envoy did not specifically request anything, every resource is retained.
165
165
  """
166
+ if len(requested) == 0:
167
+ return generated
166
168
  return [resource for resource in generated if resource_name(resource) in requested]
167
169
 
168
170
 
@@ -0,0 +1,88 @@
1
+ import inspect
2
+ from typing import Any, Dict, Optional
3
+
4
+ from pydantic import BaseModel
5
+
6
+ from sovereign.utils.entry_point_loader import EntryPointLoader
7
+ from sovereign.dynamic_config.loaders import CustomLoader
8
+ from sovereign.dynamic_config.deser import ConfigDeserializer
9
+
10
+
11
+ class Loadable(BaseModel):
12
+ path: str
13
+ protocol: str
14
+ serialization: Optional[str] = None
15
+
16
+ def load(self, default: Any = None) -> Any:
17
+ if self.protocol not in LOADERS:
18
+ raise KeyError(
19
+ f"Could not find CustomLoader {self.protocol}. Available: {LOADERS}"
20
+ )
21
+ loader_ = LOADERS[self.protocol]
22
+
23
+ ser = self.serialization
24
+ if ser is None:
25
+ ser = loader_.default_deser
26
+ if ser not in DESERIALIZERS:
27
+ raise KeyError(
28
+ f"Could not find Deserializer {ser}. Available: {DESERIALIZERS}"
29
+ )
30
+ deserializer = DESERIALIZERS[ser]
31
+
32
+ try:
33
+ data = loader_.load(self.path)
34
+ return deserializer.deserialize(data)
35
+ except Exception as original_error:
36
+ if default is not None:
37
+ return default
38
+ raise Exception(
39
+ f"{self.protocol=}, {self.path=}, {self.serialization=}, {original_error=}"
40
+ )
41
+
42
+ @staticmethod
43
+ def from_legacy_fmt(fmt_string: str) -> "Loadable":
44
+ if "://" not in fmt_string:
45
+ return Loadable(protocol="inline", serialization="string", path=fmt_string)
46
+ try:
47
+ scheme, path = fmt_string.split("://")
48
+ except ValueError:
49
+ raise ValueError(fmt_string)
50
+ try:
51
+ proto, ser = scheme.split("+")
52
+ except ValueError:
53
+ proto, ser = scheme, "yaml"
54
+
55
+ if proto in ("python", "module"):
56
+ ser = "raw"
57
+ if proto in ("http", "https"):
58
+ path = "://".join([proto, path])
59
+
60
+ return Loadable(
61
+ protocol=proto,
62
+ serialization=ser,
63
+ path=path,
64
+ )
65
+
66
+
67
+ LOADERS: Dict[str, CustomLoader] = {}
68
+ for entry_point in EntryPointLoader("loaders").groups["loaders"]:
69
+ custom_loader = entry_point.load()
70
+ func = custom_loader()
71
+ method = getattr(func, "load")
72
+ if not inspect.ismethod(method):
73
+ raise AttributeError(
74
+ f"CustomLoader {entry_point.name} does not implement .load()"
75
+ )
76
+ LOADERS[entry_point.name] = func
77
+
78
+
79
+ DESERIALIZERS: Dict[str, ConfigDeserializer] = {}
80
+ for entry_point in EntryPointLoader("deserializers").groups["deserializers"]:
81
+ deserializer = entry_point.load()
82
+ func = deserializer()
83
+ method = getattr(func, "deserialize")
84
+ if not inspect.ismethod(method):
85
+ raise AttributeError(
86
+ f"Deserializer {entry_point.name} does not implement .deserialize()"
87
+ )
88
+ DESERIALIZERS[entry_point.name] = func
@@ -0,0 +1,79 @@
1
+ import json
2
+ from typing import Any, Protocol
3
+
4
+ import yaml
5
+ import jinja2
6
+
7
+ try:
8
+ import ujson
9
+
10
+ UJSON_AVAILABLE = True
11
+ except ImportError:
12
+ UJSON_AVAILABLE = False
13
+
14
+ try:
15
+ import orjson
16
+
17
+ ORJSON_AVAILABLE = True
18
+ except ImportError:
19
+ ORJSON_AVAILABLE = False
20
+
21
+ jinja_env = jinja2.Environment(autoescape=True)
22
+
23
+
24
+ class ConfigDeserializer(Protocol):
25
+ """
26
+ Deserializers can be added to sovereign by creating a subclass
27
+ and then specified in config:
28
+
29
+ template_context:
30
+ context:
31
+ ...:
32
+ protocol: ...
33
+ serialization: <serializer name>
34
+ path: ...
35
+ """
36
+
37
+ def deserialize(self, input: Any) -> Any:
38
+ ...
39
+
40
+
41
+ class YamlDeserializer(ConfigDeserializer):
42
+ def deserialize(self, input: Any) -> Any:
43
+ return yaml.safe_load(input)
44
+
45
+
46
+ class JsonDeserializer(ConfigDeserializer):
47
+ def deserialize(self, input: Any) -> Any:
48
+ return json.loads(input)
49
+
50
+
51
+ class JinjaDeserializer(ConfigDeserializer):
52
+ def deserialize(self, input: Any) -> Any:
53
+ return jinja_env.from_string(input)
54
+
55
+
56
+ class StringDeserializer(ConfigDeserializer):
57
+ def deserialize(self, input: Any) -> Any:
58
+ return str(input)
59
+
60
+
61
+ class PassthroughDeserializer(ConfigDeserializer):
62
+ def deserialize(self, input: Any) -> Any:
63
+ return input
64
+
65
+
66
+ class UjsonDeserializer(ConfigDeserializer):
67
+ def deserialize(self, input: Any) -> Any:
68
+ if not UJSON_AVAILABLE:
69
+ raise ImportError("Configured a UJSON deserializer but it's not installed")
70
+ return ujson.loads(input)
71
+
72
+
73
+ class OrjsonDeserializer(ConfigDeserializer):
74
+ def deserialize(self, input: Any) -> Any:
75
+ if not ORJSON_AVAILABLE:
76
+ raise ImportError(
77
+ "Configured an ORJSON deserializer but it's not installed"
78
+ )
79
+ return orjson.loads(input)
@@ -0,0 +1,121 @@
1
+ import os
2
+ import importlib
3
+ from typing import Any, Protocol
4
+ from pathlib import Path
5
+ from importlib.machinery import SourceFileLoader
6
+
7
+ import requests
8
+
9
+ from sovereign.utils.resources import get_package_file_bytes
10
+
11
+ try:
12
+ import boto3
13
+
14
+ BOTO_IS_AVAILABLE = True
15
+ except ImportError:
16
+ BOTO_IS_AVAILABLE = False
17
+
18
+
19
+ class CustomLoader(Protocol):
20
+ """
21
+ Custom loaders can be added to sovereign by creating a subclass
22
+ and then in config:
23
+
24
+ template_context:
25
+ context:
26
+ ...:
27
+ protocol: <loader name>
28
+ serialization: ...
29
+ path: <path argument>
30
+ """
31
+
32
+ default_deser: str = "yaml"
33
+
34
+ def load(self, path: str) -> Any:
35
+ ...
36
+
37
+
38
+ class File(CustomLoader):
39
+ default_deser = "passthrough"
40
+
41
+ def load(self, path: str) -> Any:
42
+ with open(path) as f:
43
+ contents = f.read()
44
+ try:
45
+ return contents
46
+ except FileNotFoundError:
47
+ raise FileNotFoundError(f"Unable to load {path}")
48
+
49
+
50
+ class PackageData(CustomLoader):
51
+ default_deser = "string"
52
+
53
+ def load(self, path: str) -> Any:
54
+ pkg, pkg_file = path.split(":")
55
+ data = get_package_file_bytes(pkg, pkg_file)
56
+ return data
57
+
58
+
59
+ class Web(CustomLoader):
60
+ default_deser = "json"
61
+
62
+ def load(self, path: str) -> Any:
63
+ response = requests.get(path)
64
+ response.raise_for_status()
65
+ data = response.text
66
+ return data
67
+
68
+
69
+ class EnvironmentVariable(CustomLoader):
70
+ default_deser = "raw"
71
+
72
+ def load(self, path: str) -> Any:
73
+ data = os.getenv(path)
74
+ if data is None:
75
+ raise AttributeError(f"Unable to read environment variable {path}")
76
+ return data
77
+
78
+
79
+ class PythonModule(CustomLoader):
80
+ default_deser = "passthrough"
81
+
82
+ def load(self, path: str) -> Any:
83
+ if ":" in path:
84
+ mod, fn = path.rsplit(":", maxsplit=1)
85
+ else:
86
+ mod, fn = path, ""
87
+ imported = importlib.import_module(mod)
88
+ if fn != "":
89
+ return getattr(imported, fn)
90
+ return imported
91
+
92
+
93
+ class S3Bucket(CustomLoader):
94
+ default_deser = "raw"
95
+
96
+ def load(self, path: str) -> Any:
97
+ if not BOTO_IS_AVAILABLE:
98
+ raise ImportError(
99
+ "boto3 must be installed to load S3 paths. Use ``pip install sovereign[boto]``"
100
+ )
101
+ bucket, key = path.split("/", maxsplit=1)
102
+ s3 = boto3.client("s3")
103
+ response = s3.get_object(Bucket=bucket, Key=key)
104
+ data = "".join([chunk.decode() for chunk in response["Body"]])
105
+ return data
106
+
107
+
108
+ class PythonInlineCode(CustomLoader):
109
+ default_deser = "passthrough"
110
+
111
+ def load(self, path: str) -> Any:
112
+ p = str(Path(path).absolute())
113
+ loader = SourceFileLoader(p, path=p)
114
+ return loader.load_module(p)
115
+
116
+
117
+ class Inline(CustomLoader):
118
+ default_deser = "string"
119
+
120
+ def load(self, path: str) -> Any:
121
+ return path
sovereign/schemas.py CHANGED
@@ -20,7 +20,8 @@ from pydantic import (
20
20
  )
21
21
  from pydantic_settings import BaseSettings, SettingsConfigDict
22
22
 
23
- from sovereign.config_loader import Loadable, Serialization, jinja_env
23
+ from sovereign.dynamic_config import Loadable
24
+ from sovereign.dynamic_config.deser import jinja_env
24
25
  from sovereign.utils.crypto.suites import EncryptionType
25
26
  from sovereign.utils.version_info import compute_hash
26
27
 
@@ -111,6 +112,7 @@ class DiscoveryCacheConfig(BaseModel):
111
112
  suppress: bool = False # False = Don't suppress connection errors. True = suppress connection errors
112
113
  socket_keepalive: bool = True # Try to keep connections to redis around.
113
114
  ttl: int = 60
115
+ extra_keys: Dict[str, Any] = {}
114
116
 
115
117
  @model_validator(mode="after")
116
118
  def set_default_protocol(self) -> Self:
@@ -164,15 +166,15 @@ class XdsTemplate:
164
166
  return self.code.render(*args, **kwargs)
165
167
 
166
168
  def load_source(self) -> str:
167
- if self.loadable.serialization in (Serialization.jinja, Serialization.jinja2):
169
+ old_serialization = self.loadable.serialization
170
+ if self.loadable.serialization in ("jinja", "jinja2"):
168
171
  # The Jinja2 template serializer does not properly set a name
169
172
  # for the loaded template.
170
173
  # The repr for the template prints out as the memory address
171
174
  # This makes it really hard to generate a consistent version_info string
172
175
  # in rendered configuration.
173
176
  # For this reason, we re-load the template as a string instead, and create a checksum.
174
- old_serialization = self.loadable.serialization
175
- self.loadable.serialization = Serialization("string")
177
+ self.loadable.serialization = "string"
176
178
  ret = self.loadable.load()
177
179
  self.loadable.serialization = old_serialization
178
180
  return str(ret)
@@ -180,9 +182,8 @@ class XdsTemplate:
180
182
  # If the template specified is a python source file,
181
183
  # we can simply read and return the source of it.
182
184
  old_protocol = self.loadable.protocol
183
- old_serialization = self.loadable.serialization
184
185
  self.loadable.protocol = "inline"
185
- self.loadable.serialization = Serialization("string")
186
+ self.loadable.serialization = "string"
186
187
  ret = self.loadable.load()
187
188
  self.loadable.protocol = old_protocol
188
189
  self.loadable.serialization = old_serialization
@@ -294,7 +295,9 @@ class Resources(List[str]):
294
295
  """
295
296
 
296
297
  def __contains__(self, item: object) -> bool:
297
- if len(self) == 0:
298
+ if (
299
+ len(self) == 0
300
+ ): # TODO: refactor to remove overriding __contains__; its being used in multiple places
298
301
  return True
299
302
  return super().__contains__(item)
300
303
 
@@ -305,13 +308,17 @@ class Status(BaseModel):
305
308
  details: List[Any]
306
309
 
307
310
 
311
+ def resources_factory() -> Resources:
312
+ return Resources()
313
+
314
+
308
315
  class DiscoveryRequest(BaseModel):
309
316
  node: Node = Field(..., title="Node information about the envoy proxy")
310
317
  version_info: str = Field(
311
318
  "0", title="The version of the envoy clients current configuration"
312
319
  )
313
- resource_names: list[str] | Resources = Field(
314
- Resources(), title="List of requested resource names"
320
+ resource_names: Resources = Field(
321
+ default_factory=resources_factory, title="List of requested resource names"
315
322
  )
316
323
  hide_private_keys: bool = False
317
324
  type_url: Optional[str] = Field(
@@ -353,6 +360,11 @@ class DiscoveryRequest(BaseModel):
353
360
  self.desired_controlplane,
354
361
  )
355
362
 
363
+ @field_validator("resource_names", mode="before")
364
+ @classmethod
365
+ def validate_resources(cls, v: Union[Resources, List[str]]) -> Resources:
366
+ return Resources(v)
367
+
356
368
 
357
369
  class DiscoveryResponse(BaseModel):
358
370
  version_info: str = Field(
@@ -447,6 +459,7 @@ class SovereignConfig(BaseSettings):
447
459
  log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
448
460
  ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
449
461
  discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
462
+ tracing: Optional["TracingConfig"] = None
450
463
  model_config = SettingsConfigDict(
451
464
  env_file=".env",
452
465
  extra="ignore",
@@ -633,6 +646,46 @@ class SourcesConfiguration(BaseSettings):
633
646
  )
634
647
 
635
648
 
649
+ class TracingConfig(BaseSettings):
650
+ enabled: bool = Field(False)
651
+ collector: str = Field("notset")
652
+ endpoint: str = Field("/v2/api/spans")
653
+ trace_id_128bit: bool = Field(True)
654
+ tags: Dict[str, Union[Loadable, str]] = dict()
655
+ model_config = SettingsConfigDict(
656
+ env_file=".env",
657
+ extra="ignore",
658
+ env_file_encoding="utf-8",
659
+ populate_by_name=True,
660
+ )
661
+
662
+ @field_validator("tags", mode="before")
663
+ @classmethod
664
+ def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
665
+ ret = dict()
666
+ for key, value in v.items():
667
+ if isinstance(value, dict):
668
+ ret[key] = Loadable(**value).load()
669
+ elif isinstance(value, str):
670
+ ret[key] = Loadable.from_legacy_fmt(value).load()
671
+ else:
672
+ raise ValueError(f"Received an invalid tag for tracing: {value}")
673
+ return ret
674
+
675
+ @model_validator(mode="before")
676
+ @classmethod
677
+ def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
678
+ if enabled := getenv("SOVEREIGN_TRACING_ENABLED"):
679
+ values["enabled"] = enabled
680
+ if collector := getenv("SOVEREIGN_TRACING_COLLECTOR"):
681
+ values["collector"] = collector
682
+ if endpoint := getenv("SOVEREIGN_TRACING_ENDPOINT"):
683
+ values["endpoint"] = endpoint
684
+ if trace_id_128bit := getenv("SOVEREIGN_TRACING_TRACE_ID_128BIT"):
685
+ values["trace_id_128bit"] = trace_id_128bit
686
+ return values
687
+
688
+
636
689
  class LegacyConfig(BaseSettings):
637
690
  regions: Optional[List[str]] = None
638
691
  eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
@@ -720,6 +773,7 @@ class SovereignConfigv2(BaseSettings):
720
773
  debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
721
774
  legacy_fields: LegacyConfig = LegacyConfig()
722
775
  discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
776
+ tracing: TracingConfig = TracingConfig()
723
777
  model_config = SettingsConfigDict(
724
778
  env_file=".env",
725
779
  extra="ignore",
@@ -810,6 +864,7 @@ class SovereignConfigv2(BaseSettings):
810
864
  statsd=other.statsd,
811
865
  sentry_dsn=SecretStr(other.sentry_dsn),
812
866
  debug=other.debug_enabled,
867
+ tracing=other.tracing,
813
868
  legacy_fields=LegacyConfig(
814
869
  regions=other.regions,
815
870
  eds_priority_matrix=other.eds_priority_matrix,
sovereign/sources/file.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from typing import Any, Dict
2
2
  from sovereign.sources.lib import Source
3
- from sovereign.config_loader import Loadable
3
+ from sovereign.dynamic_config import Loadable
4
4
 
5
5
 
6
6
  class File(Source):
@@ -88,14 +88,24 @@
88
88
  1: 'resource'
89
89
  } %}
90
90
  {% for resource in res %}
91
+ {% if "sovereign_error" in resource %}
92
+ <span class="panel-icon">
93
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
94
+ </span>
95
+ <div class="notification is-danger">
96
+ {{ resource["sovereign_error"] }}
97
+ </div>
98
+ {{ resource["sovereign_error"] }}
99
+ {% else %}
91
100
  {% set name = resource.get('name') or resource['cluster_name'] %}
92
101
  <a class="panel-block has-text-weight-medium"
93
- href="/ui/resources/{{ resource_type }}/{{ name }}">
94
- <span class="panel-icon">
95
- <i class="fas fa-arrow-right" aria-hidden="true"></i>
96
- </span>
97
- {{ name }}
98
- </a>
102
+ href="/ui/resources/{{ resource_type }}/{{ name }}">
103
+ <span class="panel-icon">
104
+ <i class="fas fa-arrow-right" aria-hidden="true"></i>
105
+ </span>
106
+ {{ name }}
107
+ </a>
108
+ {% endif %}
99
109
  {% endfor %}
100
110
  <div class="panel-block">
101
111
  <p class="content is-small">
@@ -1,9 +1,8 @@
1
1
  from typing import Any
2
- from sovereign.config_loader import CustomLoader, Serialization
2
+ from sovereign.dynamic_config.loaders import CustomLoader
3
3
 
4
4
 
5
5
  class Multiply(CustomLoader):
6
- @staticmethod
7
- def load(path: str, ser: Serialization) -> Any:
6
+ def load(self, path: str) -> Any:
8
7
  result = path * 2
9
- return f"{ser}:{result}"
8
+ return result
sovereign/tracing.py ADDED
@@ -0,0 +1,85 @@
1
+ import time
2
+ import uuid
3
+ import requests
4
+ from contextvars import ContextVar
5
+ from contextlib import nullcontext
6
+
7
+ from sovereign import config
8
+
9
+ _trace_id_ctx_var: ContextVar[str] = ContextVar("trace_id", default="")
10
+ _span_id_ctx_var: ContextVar[str] = ContextVar("span_id", default="")
11
+
12
+
13
+ def get_trace_id() -> str:
14
+ return _trace_id_ctx_var.get()
15
+
16
+
17
+ def get_span_id() -> str:
18
+ return _span_id_ctx_var.get()
19
+
20
+
21
+ def generate_128bit():
22
+ return str(uuid.uuid4()).replace("-", "")
23
+
24
+
25
+ def generate_64bit():
26
+ return generate_128bit()[:32]
27
+
28
+
29
+ def timestamp():
30
+ return str(time.time()).replace(".", "")
31
+
32
+
33
+ TRACING = config.tracing
34
+ TRACING_DISABLED = not TRACING.enabled
35
+
36
+
37
+ class Tracer:
38
+ def gen_id(self):
39
+ if TRACING.trace_id_128bit:
40
+ trace_id = generate_128bit()
41
+ else:
42
+ trace_id = generate_64bit()
43
+ _trace_id_ctx_var.set(trace_id)
44
+ return trace_id
45
+
46
+ def __init__(self, span_name):
47
+ if TRACING_DISABLED:
48
+ return
49
+ span_id = get_span_id()
50
+ self.parent_span_id = None
51
+ if span_id != "":
52
+ # We are already inside a trace context
53
+ self.parent_span_id = span_id
54
+ self.trace_id = get_trace_id()
55
+ self.span_id = self.gen_id()
56
+ self.span_name = span_name
57
+
58
+ def __enter__(self):
59
+ if TRACING_DISABLED:
60
+ return nullcontext()
61
+ self.trace = {
62
+ "traceId": self.trace_id,
63
+ "id": self.span_id,
64
+ "name": self.span_name,
65
+ "timestamp": time.time(),
66
+ "tags": TRACING.tags,
67
+ }
68
+ if self.parent_span_id:
69
+ self.trace["parent_span_id"] = self.parent_span_id
70
+ return self
71
+
72
+ def __exit__(self, exc_type, exc_value, traceback):
73
+ if TRACING_DISABLED:
74
+ return
75
+ self.trace["duration"] = time.time() - self.trace["timestamp"]
76
+ self.submit()
77
+
78
+ def submit(self):
79
+ print(f"{self.span_name}: {self.trace['duration']}")
80
+ try:
81
+ url = f"{TRACING.collector}{TRACING.endpoint}"
82
+ requests.post(url, json=self.trace)
83
+ # pylint: disable=broad-except
84
+ except Exception as e:
85
+ print(f"Failed to submit trace: {self.trace}, Error:{e}")
@@ -117,21 +117,24 @@ async def perform_discovery(
117
117
  authenticate(req)
118
118
  if discovery_cache.enabled:
119
119
  logs.access_logger.queue_log_fields(CACHE_XDS_HIT=False)
120
- cache_key = compute_hash(
121
- [
122
- api_version,
123
- resource_type,
124
- req.envoy_version,
125
- req.resource_names,
126
- req.desired_controlplane,
127
- req.hide_private_keys,
128
- req.type_url,
129
- req.node.cluster,
130
- req.node.locality,
131
- req.node.metadata.get("auth", None),
132
- req.node.metadata.get("num_cpus", None),
133
- ]
134
- )
120
+ metadata_keys = discovery_cache.extra_keys.get("metadata", [])
121
+ extra_metadata = [req.node.metadata.get(key, None) for key in metadata_keys]
122
+ hash_keys = [
123
+ api_version,
124
+ resource_type,
125
+ req.envoy_version,
126
+ req.resource_names,
127
+ req.desired_controlplane,
128
+ req.hide_private_keys,
129
+ req.type_url,
130
+ req.node.cluster,
131
+ req.node.locality,
132
+ req.node.metadata.get("auth", None),
133
+ req.node.metadata.get("num_cpus", None),
134
+ ] + extra_metadata
135
+
136
+ cache_key = compute_hash(hash_keys)
137
+
135
138
  if template := await cache.get(key=cache_key, default=None):
136
139
  logs.access_logger.queue_log_fields(CACHE_XDS_HIT=True)
137
140
  return template # type: ignore[no-any-return]
@@ -103,8 +103,8 @@ async def resources(
103
103
  resource_type=xds_type,
104
104
  skip_auth=True,
105
105
  )
106
- except KeyError:
107
- ret["resources"] = []
106
+ except KeyError as e:
107
+ ret["resources"] = [{"sovereign_error": str(e)}]
108
108
  else:
109
109
  ret["resources"] += response.deserialize_resources()
110
110
  return html_templates.TemplateResponse(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 0.27.1
3
+ Version: 0.29.0
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
@@ -18,6 +18,7 @@ Classifier: Natural Language :: English
18
18
  Classifier: Operating System :: POSIX :: Linux
19
19
  Classifier: Programming Language :: Python :: 3
20
20
  Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
21
22
  Classifier: Programming Language :: Python :: 3.10
22
23
  Classifier: Programming Language :: Python :: 3.8
23
24
  Classifier: Programming Language :: Python :: 3.9
@@ -37,7 +38,7 @@ Requires-Dist: cachelib (>=0.10.2,<0.11.0)
37
38
  Requires-Dist: cachetools (>=5.3.2,<6.0.0)
38
39
  Requires-Dist: cashews[redis] (>=6.3.0,<7.0.0) ; extra == "caching"
39
40
  Requires-Dist: croniter (>=1.4.1,<2.0.0)
40
- Requires-Dist: cryptography (>=42.0.5,<43.0.0)
41
+ Requires-Dist: cryptography (>=43.0.1,<44.0.0)
41
42
  Requires-Dist: datadog (>=0.47.0,<0.48.0) ; extra == "statsd"
42
43
  Requires-Dist: fastapi (>=0.110.0,<0.111.0)
43
44
  Requires-Dist: glom (>=23.3.0,<24.0.0)
@@ -1,10 +1,12 @@
1
- sovereign/__init__.py,sha256=IvupftZBbnMO1wcpqXuQkmdabCGpRfGhMzYBPCCng38,3254
1
+ sovereign/__init__.py,sha256=L0PLw5Su5XX_HdSEXCJVtbCitxUMjZP5nlyl21sQ3eA,3256
2
2
  sovereign/app.py,sha256=udDhuprAcJdYNgXufl94-oh-G74H9hXVzr8cKhgdQYI,4087
3
- sovereign/config_loader.py,sha256=PeaDp_BEU7TsHFdteHWWF8a6TkIkF8z3H7LqkzowcHc,6263
4
- sovereign/configuration.py,sha256=lZce28KrlNehaWbOJIw-znoyy1pVKM0Gdwrsa236k0c,2482
3
+ sovereign/configuration.py,sha256=BCezlWYIpTsFRZwQIBwU-XrfBk1MdjTKMLA8huN-VPg,2484
5
4
  sovereign/constants.py,sha256=qdWD1lTvkaW5JGF7TmZhfksQHlRAJFVqbG7v6JQA9k8,46
6
- sovereign/context.py,sha256=NFv9n-T012Ky-P8y1DhiaUGCqa8Kk3IaI0AYcSpmKm4,6951
7
- sovereign/discovery.py,sha256=TPecAoUDHx6SbS5hE2K73uEgDro0Uzz3qsvOQMpFtGI,5899
5
+ sovereign/context.py,sha256=Vwr-Jmo1I_05rFraxv6GoYEC83oFl87LG16px4R7IfA,6461
6
+ sovereign/discovery.py,sha256=KHnWXUlceUglbR9eV5Q6LjPOpGv2dcrGOSus9-QLQJM,5952
7
+ sovereign/dynamic_config/__init__.py,sha256=QoRNcuCAqV26zeyHm0iavsR55K3TwMohabWpPGIq_rM,2838
8
+ sovereign/dynamic_config/deser.py,sha256=CYTP9UNx8falCXU_bEaWGNatyQlYrV4T57NPXNhTn0o,1842
9
+ sovereign/dynamic_config/loaders.py,sha256=HxDT-6hlqg_ewPjrFu2RaWi6O1mmJ_Mpnu8AQk_enNg,2923
8
10
  sovereign/error_info.py,sha256=r2KXBYq9Fo7AI2pmIpATWFm0pykr2MqfrKH0WWW5Sfk,1488
9
11
  sovereign/logging/access_logger.py,sha256=JMMzQvi7doFJGA__YYqyasdfAT9W31Ycu_oZ2ovAMis,2565
10
12
  sovereign/logging/application_logger.py,sha256=VI8EBRv_dB8vvTnSh3n-IaFtVh-GaNN4atjtLJdr2kI,1843
@@ -15,10 +17,10 @@ sovereign/middlewares.py,sha256=UoLdfhqMj_E6jXgtr-n0maQIBYe9n95s3BwaQZfebHo,3097
15
17
  sovereign/modifiers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
18
  sovereign/modifiers/lib.py,sha256=DbXsxrrjnFE4Y7rbwpeiM5tS5w5NBwSdYH58AtDTP0I,2884
17
19
  sovereign/response_class.py,sha256=beMAFV-4L6DwyWzJzy71GkEW4gb7fzH1jd8-Tul13cU,427
18
- sovereign/schemas.py,sha256=MD1ECS3oP1qjIy0fk_EF-WSLEe9rsMHW6vmgPdZ8nwY,29601
20
+ sovereign/schemas.py,sha256=nkpMVLwdtODmyfbwu5PEExxFKFLY3H0Oj4mqB9HJnfI,31552
19
21
  sovereign/server.py,sha256=z8Uz1UYIZix0S40Srk774WIMDN2jl2SozO8irib0wc4,1402
20
22
  sovereign/sources/__init__.py,sha256=g9hEpFk8j5i1ApHQpbc9giTyJW41Ppgsqv5P9zGxOJk,78
21
- sovereign/sources/file.py,sha256=A4UWoRU39v2Ex5Mtdl_uw53iMkslYylF4CiiwW7LOpk,689
23
+ sovereign/sources/file.py,sha256=prUThsDCSPNwZaZpkKXhAm-GVRZWbBoGKGU0It4HHXs,690
22
24
  sovereign/sources/inline.py,sha256=bNqVZyelcUofYBWHFOUIhOUU9az32CdBEfaYRzNzFFE,1002
23
25
  sovereign/sources/lib.py,sha256=LIbnlKkL0bQT10y4GT2E8yypjYxqfJYbB9FkGB5C2oc,1030
24
26
  sovereign/sources/poller.py,sha256=61zV8oHzvC0x453kN8dyfJaOSnykgXHuKiMtLRAHt0w,11059
@@ -27,10 +29,11 @@ sovereign/static/style.css,sha256=vG8HPsbCbPIZfHgy7gSeof97Pnp0okkyaXyJzIEEW-8,44
27
29
  sovereign/statistics.py,sha256=Xfj4oWMfCkbYc2ibF7rDUpbw6Zw6dI4N5BpCLDQc4j4,2336
28
30
  sovereign/templates/base.html,sha256=5vw3-NmN291pXRdArpCwhSce9bAYBWCJVRhvO5EmE9g,2296
29
31
  sovereign/templates/err.html,sha256=a3cEzOqyqWOIe3YxfTEjkxbTfxBxq1knD6GwzEFljfs,603
30
- sovereign/templates/resources.html,sha256=NnrnamWg_vJjY88efsMcjNsldg-K9TZnp6tFS5tkZOU,6366
32
+ sovereign/templates/resources.html,sha256=r67x1FeUgczh-0JlwFFu-qG_b2u9t-_P_If9jlKg2ZI,6832
31
33
  sovereign/templates/ul_filter.html,sha256=LrzZv5408Qq5UP4lcHVRwY2G6lXd3IiSNiJn1aH7Yqo,666
32
- sovereign/testing/loaders.py,sha256=3jBLqq7ZbUJSOAaX0VjDUlYAWWuJ9xetqkanzeoYiR4,248
34
+ sovereign/testing/loaders.py,sha256=mcmErhI9ZkJUBZl8jv2qP-PCBRFeAIgyBFlfCgU4Vvk,199
33
35
  sovereign/testing/modifiers.py,sha256=7_c2hWXn_sYJ6997N1_uSWtClOikcOzu1yRCY56-l-4,361
36
+ sovereign/tracing.py,sha256=KHpqddxUm8A6ucIqnY7xo44ULH7K6ZGdyayObOdDwcw,2239
34
37
  sovereign/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
38
  sovereign/utils/auth.py,sha256=sQC8eLPWtk0RIXKwwxnYqILUvUCOaEGtGrtdJflat8E,1692
36
39
  sovereign/utils/crypto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -52,11 +55,11 @@ sovereign/utils/weighted_clusters.py,sha256=bPzuRE7Qgvv04HcR2AhMDvBrFlZ8AfteweLK
52
55
  sovereign/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
56
  sovereign/views/admin.py,sha256=9jUI3YqaU42AtzCCOCKDcfj_2JXoaMU6eOAD6WYPjoI,4312
54
57
  sovereign/views/crypto.py,sha256=o8NSyiUBy7v1pMOXt_1UBi68FNcGkXSlEVg9C18y8kY,3324
55
- sovereign/views/discovery.py,sha256=TVvWTMzWydsC-SNKL9WsSss_Hfnt2Ed4SVC2A8Na7Jo,5932
58
+ sovereign/views/discovery.py,sha256=9TyXfS3nW7I7Bkeg-KdsTeCm1N9WSCvDj_pTAi1V4bo,6067
56
59
  sovereign/views/healthchecks.py,sha256=_WkMunlrFpqGTLgtNtRr7gCsDCv5kiuYxCyTi-dMEKM,1357
57
- sovereign/views/interface.py,sha256=Xi5F36Zn7ATbMtuhlE8f9xTEKj9T7GzHg6XLdb3bQjY,7023
58
- sovereign-0.27.1.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
59
- sovereign-0.27.1.dist-info/METADATA,sha256=GWNMRETzLX-fAW9Z-VOOclNAoXZKFwwgvtwerzKsyXo,6556
60
- sovereign-0.27.1.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
61
- sovereign-0.27.1.dist-info/entry_points.txt,sha256=T6HkcZHqKSDcK9d23svUU-vt-_DQbSV0SRt4_UtQGEc,289
62
- sovereign-0.27.1.dist-info/RECORD,,
60
+ sovereign/views/interface.py,sha256=TFXbYp5oXZPRkVnAo-NWQFBb8XMtB519FaV78ludCcI,7055
61
+ sovereign-0.29.0.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
62
+ sovereign-0.29.0.dist-info/METADATA,sha256=M3LgSzH5wpBmLWUwWy_bPFclUSmnsz5SfnoH3WJ8fZ4,6607
63
+ sovereign-0.29.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
64
+ sovereign-0.29.0.dist-info/entry_points.txt,sha256=2mUHQjqeXEokMF6ZjDmvqQ9Fxk-Or2S4eC0h70ZxKmk,1201
65
+ sovereign-0.29.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 1.7.0
2
+ Generator: poetry-core 1.9.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,32 @@
1
+ [console_scripts]
2
+ sovereign=sovereign.server:main
3
+
4
+ [sovereign.deserializers]
5
+ jinja=sovereign.dynamic_config.deser:JinjaDeserializer
6
+ jinja2=sovereign.dynamic_config.deser:JinjaDeserializer
7
+ json=sovereign.dynamic_config.deser:JsonDeserializer
8
+ orjson=sovereign.dynamic_config.deser:OrjsonDeserializer
9
+ raw=sovereign.dynamic_config.deser:PassthroughDeserializer
10
+ string=sovereign.dynamic_config.deser:StringDeserializer
11
+ ujson=sovereign.dynamic_config.deser:UjsonDeserializer
12
+ yaml=sovereign.dynamic_config.deser:YamlDeserializer
13
+
14
+ [sovereign.loaders]
15
+ env=sovereign.dynamic_config.loaders:EnvironmentVariable
16
+ example=sovereign.testing.loaders:Multiply
17
+ file=sovereign.dynamic_config.loaders:File
18
+ http=sovereign.dynamic_config.loaders:Web
19
+ https=sovereign.dynamic_config.loaders:Web
20
+ inline=sovereign.dynamic_config.loaders:Inline
21
+ module=sovereign.dynamic_config.loaders:PythonModule
22
+ pkgdata=sovereign.dynamic_config.loaders:PackageData
23
+ python=sovereign.dynamic_config.loaders:PythonInlineCode
24
+ s3=sovereign.dynamic_config.loaders:S3Bucket
25
+
26
+ [sovereign.modifiers]
27
+ sovereign_3rd_party_test=sovereign.testing.modifiers:Test
28
+
29
+ [sovereign.sources]
30
+ file=sovereign.sources.file:File
31
+ inline=sovereign.sources.inline:Inline
32
+
@@ -1,222 +0,0 @@
1
- import os
2
- import json
3
- from enum import Enum
4
- from typing import Any, Dict, Callable, Union, Protocol
5
- from types import ModuleType
6
- import yaml
7
- import jinja2
8
- import requests
9
- import importlib
10
- from importlib.machinery import SourceFileLoader
11
- from pathlib import Path
12
- from pydantic import BaseModel
13
- from sovereign.utils.resources import get_package_file_bytes
14
-
15
-
16
- class Serialization(Enum):
17
- """
18
- Types of deserialization available in Sovereign
19
- for loading configuration field values.
20
- """
21
-
22
- yaml = "yaml"
23
- json = "json"
24
- orjson = "orjson"
25
- ujson = "ujson"
26
- jinja = "jinja"
27
- jinja2 = "jinja2"
28
- string = "string"
29
- raw = "raw"
30
- skip = "skip"
31
-
32
-
33
- jinja_env = jinja2.Environment(autoescape=True)
34
-
35
-
36
- def passthrough(item: Any) -> Any:
37
- return item
38
-
39
-
40
- def string(item: Any) -> Any:
41
- return str(item)
42
-
43
-
44
- serializers: Dict[Serialization, Callable[[Any], Any]] = {
45
- Serialization.yaml: yaml.safe_load,
46
- Serialization.json: json.loads,
47
- Serialization.jinja: jinja_env.from_string,
48
- Serialization.jinja2: jinja_env.from_string,
49
- Serialization.string: string,
50
- Serialization.raw: passthrough,
51
- }
52
-
53
-
54
- try:
55
- import ujson
56
-
57
- serializers[Serialization.ujson] = ujson.loads
58
- jinja_env.policies["json.dumps_function"] = ujson.dumps
59
- except ImportError:
60
- # This lambda will raise an exception when the serializer is used; otherwise we should not crash
61
- serializers[Serialization.ujson] = lambda *a, **kw: raise_(
62
- ImportError("ujson must be installed to use in config_loaders")
63
- )
64
-
65
- try:
66
- import orjson
67
-
68
- serializers[Serialization.orjson] = orjson.loads
69
-
70
- # orjson.dumps returns bytes, so we have to wrap & decode it
71
- def orjson_dumps(*args: Any, **kwargs: Any) -> Any:
72
- try:
73
- representation = orjson.dumps(*args, **kwargs)
74
- except TypeError:
75
- raise TypeError(f"Unable to dump objects using ORJSON: {args}, {kwargs}")
76
- try:
77
- return representation.decode()
78
- except Exception as e:
79
- raise e.__class__(
80
- f"Unable to decode ORJSON: {representation!r}. Original exception: {e}"
81
- )
82
-
83
- jinja_env.policies["json.dumps_function"] = orjson_dumps
84
- jinja_env.policies["json.dumps_kwargs"] = {"option": orjson.OPT_SORT_KEYS}
85
- except ImportError:
86
- # This lambda will raise an exception when the serializer is used; otherwise we should not crash
87
- serializers[Serialization.orjson] = lambda *a, **kw: raise_(
88
- ImportError("orjson must be installed to use in config_loaders")
89
- )
90
-
91
- try:
92
- import boto3
93
- except ImportError:
94
- boto3 = None
95
-
96
-
97
- class CustomLoader(Protocol):
98
- def load(self, path: str, ser: Serialization) -> Any:
99
- ...
100
-
101
-
102
- class Loadable(BaseModel):
103
- protocol: str = "http"
104
- serialization: Serialization = Serialization.yaml
105
- path: str
106
-
107
- def load(self, default: Any = None) -> Any:
108
- try:
109
- return loaders[self.protocol](self.path, self.serialization)
110
- except Exception:
111
- if default is not None:
112
- return default
113
- raise
114
-
115
- @staticmethod
116
- def from_legacy_fmt(fmt_string: str) -> "Loadable":
117
- if "://" not in fmt_string:
118
- return Loadable(
119
- protocol="inline", serialization=Serialization.string, path=fmt_string
120
- )
121
- try:
122
- scheme, path = fmt_string.split("://")
123
- except ValueError:
124
- raise ValueError(fmt_string)
125
- try:
126
- proto, ser = scheme.split("+")
127
- except ValueError:
128
- proto, ser = scheme, "yaml"
129
-
130
- serialization: Serialization = Serialization(ser)
131
- if proto in ("python", "module"):
132
- serialization = Serialization.raw
133
- if proto in ("http", "https"):
134
- path = "://".join([proto, path])
135
-
136
- return Loadable(
137
- protocol=proto,
138
- serialization=serialization,
139
- path=path,
140
- )
141
-
142
-
143
- def raise_(e: Exception) -> Exception:
144
- raise e
145
-
146
-
147
- def load_file(path: str, ser: Serialization) -> Any:
148
- with open(path) as f:
149
- contents = f.read()
150
- try:
151
- return serializers[ser](contents)
152
- except FileNotFoundError:
153
- raise FileNotFoundError(f"Unable to load {path}")
154
-
155
-
156
- def load_package_data(path: str, ser: Serialization) -> Any:
157
- pkg, pkg_file = path.split(":")
158
- data = get_package_file_bytes(pkg, pkg_file)
159
- return serializers[ser](data)
160
-
161
-
162
- def load_http(path: str, ser: Serialization) -> Any:
163
- response = requests.get(path)
164
- response.raise_for_status()
165
- data = response.text
166
- return serializers[ser](data)
167
-
168
-
169
- def load_env(variable: str, ser: Serialization = Serialization.raw) -> Any:
170
- data = os.getenv(variable)
171
- try:
172
- return serializers[ser](data)
173
- except AttributeError as e:
174
- raise AttributeError(
175
- f"Unable to read environment variable {variable}: {repr(e)}"
176
- )
177
-
178
-
179
- def load_module(name: str, _: Serialization = Serialization.raw) -> Any:
180
- if ":" in name:
181
- mod, fn = name.rsplit(":", maxsplit=1)
182
- else:
183
- mod, fn = name, ""
184
- imported = importlib.import_module(mod)
185
- if fn != "":
186
- return getattr(imported, fn)
187
- return imported
188
-
189
-
190
- def load_s3(path: str, ser: Serialization = Serialization.raw) -> Any:
191
- if isinstance(boto3, type(None)):
192
- raise ImportError(
193
- "boto3 must be installed to load S3 paths. Use ``pip install sovereign[boto]``"
194
- )
195
- bucket, key = path.split("/", maxsplit=1)
196
- s3 = boto3.client("s3")
197
- response = s3.get_object(Bucket=bucket, Key=key)
198
- data = "".join([chunk.decode() for chunk in response["Body"]])
199
- return serializers[ser](data)
200
-
201
-
202
- def load_python(path: str, _: Serialization = Serialization.raw) -> ModuleType:
203
- p = str(Path(path).absolute())
204
- loader = SourceFileLoader(p, path=p)
205
- return loader.load_module(p)
206
-
207
-
208
- def load_inline(path: str, _: Serialization = Serialization.raw) -> Any:
209
- return str(path)
210
-
211
-
212
- loaders: Dict[str, Callable[[str, Serialization], Union[str, Any]]] = {
213
- "file": load_file,
214
- "pkgdata": load_package_data,
215
- "http": load_http,
216
- "https": load_http,
217
- "env": load_env,
218
- "module": load_module,
219
- "s3": load_s3,
220
- "python": load_python,
221
- "inline": load_inline,
222
- }
@@ -1,13 +0,0 @@
1
- [console_scripts]
2
- sovereign=sovereign.server:main
3
-
4
- [sovereign.loaders]
5
- example=sovereign.testing.loaders:Multiply
6
-
7
- [sovereign.modifiers]
8
- sovereign_3rd_party_test=sovereign.testing.modifiers:Test
9
-
10
- [sovereign.sources]
11
- file=sovereign.sources.file:File
12
- inline=sovereign.sources.inline:Inline
13
-