sovereign 0.28.0__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 +2 -2
- sovereign/configuration.py +2 -2
- sovereign/context.py +1 -11
- sovereign/dynamic_config/__init__.py +88 -0
- sovereign/dynamic_config/deser.py +79 -0
- sovereign/dynamic_config/loaders.py +121 -0
- sovereign/schemas.py +50 -6
- sovereign/sources/file.py +1 -1
- sovereign/testing/loaders.py +3 -4
- sovereign/tracing.py +85 -0
- sovereign/views/discovery.py +18 -15
- {sovereign-0.28.0.dist-info → sovereign-0.29.0.dist-info}/METADATA +3 -2
- {sovereign-0.28.0.dist-info → sovereign-0.29.0.dist-info}/RECORD +16 -13
- {sovereign-0.28.0.dist-info → sovereign-0.29.0.dist-info}/WHEEL +1 -1
- sovereign-0.29.0.dist-info/entry_points.txt +32 -0
- sovereign/config_loader.py +0 -222
- sovereign-0.28.0.dist-info/entry_points.txt +0 -13
- {sovereign-0.28.0.dist-info → sovereign-0.29.0.dist-info}/LICENSE.txt +0 -0
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
|
|
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 =
|
|
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
|
|
sovereign/configuration.py
CHANGED
|
@@ -3,7 +3,7 @@ from typing import Any, Mapping
|
|
|
3
3
|
|
|
4
4
|
from pydantic import ValidationError
|
|
5
5
|
|
|
6
|
-
from sovereign import
|
|
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 =
|
|
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.
|
|
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:
|
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
@@ -458,6 +459,7 @@ class SovereignConfig(BaseSettings):
|
|
|
458
459
|
log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
|
|
459
460
|
ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
|
|
460
461
|
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
462
|
+
tracing: Optional["TracingConfig"] = None
|
|
461
463
|
model_config = SettingsConfigDict(
|
|
462
464
|
env_file=".env",
|
|
463
465
|
extra="ignore",
|
|
@@ -644,6 +646,46 @@ class SourcesConfiguration(BaseSettings):
|
|
|
644
646
|
)
|
|
645
647
|
|
|
646
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
|
+
|
|
647
689
|
class LegacyConfig(BaseSettings):
|
|
648
690
|
regions: Optional[List[str]] = None
|
|
649
691
|
eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
|
|
@@ -731,6 +773,7 @@ class SovereignConfigv2(BaseSettings):
|
|
|
731
773
|
debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
|
|
732
774
|
legacy_fields: LegacyConfig = LegacyConfig()
|
|
733
775
|
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
776
|
+
tracing: TracingConfig = TracingConfig()
|
|
734
777
|
model_config = SettingsConfigDict(
|
|
735
778
|
env_file=".env",
|
|
736
779
|
extra="ignore",
|
|
@@ -821,6 +864,7 @@ class SovereignConfigv2(BaseSettings):
|
|
|
821
864
|
statsd=other.statsd,
|
|
822
865
|
sentry_dsn=SecretStr(other.sentry_dsn),
|
|
823
866
|
debug=other.debug_enabled,
|
|
867
|
+
tracing=other.tracing,
|
|
824
868
|
legacy_fields=LegacyConfig(
|
|
825
869
|
regions=other.regions,
|
|
826
870
|
eds_priority_matrix=other.eds_priority_matrix,
|
sovereign/sources/file.py
CHANGED
sovereign/testing/loaders.py
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
from typing import Any
|
|
2
|
-
from sovereign.
|
|
2
|
+
from sovereign.dynamic_config.loaders import CustomLoader
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class Multiply(CustomLoader):
|
|
6
|
-
|
|
7
|
-
def load(path: str, ser: Serialization) -> Any:
|
|
6
|
+
def load(self, path: str) -> Any:
|
|
8
7
|
result = path * 2
|
|
9
|
-
return
|
|
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}")
|
sovereign/views/discovery.py
CHANGED
|
@@ -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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: sovereign
|
|
3
|
-
Version: 0.
|
|
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 (>=
|
|
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=
|
|
1
|
+
sovereign/__init__.py,sha256=L0PLw5Su5XX_HdSEXCJVtbCitxUMjZP5nlyl21sQ3eA,3256
|
|
2
2
|
sovereign/app.py,sha256=udDhuprAcJdYNgXufl94-oh-G74H9hXVzr8cKhgdQYI,4087
|
|
3
|
-
sovereign/
|
|
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=
|
|
5
|
+
sovereign/context.py,sha256=Vwr-Jmo1I_05rFraxv6GoYEC83oFl87LG16px4R7IfA,6461
|
|
7
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=
|
|
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=
|
|
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
|
|
@@ -29,8 +31,9 @@ sovereign/templates/base.html,sha256=5vw3-NmN291pXRdArpCwhSce9bAYBWCJVRhvO5EmE9g
|
|
|
29
31
|
sovereign/templates/err.html,sha256=a3cEzOqyqWOIe3YxfTEjkxbTfxBxq1knD6GwzEFljfs,603
|
|
30
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=
|
|
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=
|
|
58
|
+
sovereign/views/discovery.py,sha256=9TyXfS3nW7I7Bkeg-KdsTeCm1N9WSCvDj_pTAi1V4bo,6067
|
|
56
59
|
sovereign/views/healthchecks.py,sha256=_WkMunlrFpqGTLgtNtRr7gCsDCv5kiuYxCyTi-dMEKM,1357
|
|
57
60
|
sovereign/views/interface.py,sha256=TFXbYp5oXZPRkVnAo-NWQFBb8XMtB519FaV78ludCcI,7055
|
|
58
|
-
sovereign-0.
|
|
59
|
-
sovereign-0.
|
|
60
|
-
sovereign-0.
|
|
61
|
-
sovereign-0.
|
|
62
|
-
sovereign-0.
|
|
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,,
|
|
@@ -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
|
+
|
sovereign/config_loader.py
DELETED
|
@@ -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
|
-
|
|
File without changes
|