sovereign 0.28.0__tar.gz → 0.29.0a3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of sovereign might be problematic. Click here for more details.
- {sovereign-0.28.0 → sovereign-0.29.0a3}/PKG-INFO +1 -1
- {sovereign-0.28.0 → sovereign-0.29.0a3}/pyproject.toml +21 -1
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/__init__.py +2 -2
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/configuration.py +2 -2
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/context.py +1 -11
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/discovery.py +16 -8
- sovereign-0.29.0a3/src/sovereign/dynamic_config/__init__.py +85 -0
- sovereign-0.29.0a3/src/sovereign/dynamic_config/deser.py +79 -0
- sovereign-0.29.0a3/src/sovereign/dynamic_config/loaders.py +121 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/schemas.py +49 -6
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/sources/file.py +1 -1
- sovereign-0.29.0a3/src/sovereign/testing/loaders.py +8 -0
- sovereign-0.29.0a3/src/sovereign/tracing.py +85 -0
- sovereign-0.28.0/src/sovereign/config_loader.py +0 -222
- sovereign-0.28.0/src/sovereign/testing/loaders.py +0 -9
- {sovereign-0.28.0 → sovereign-0.29.0a3}/LICENSE.txt +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/README.md +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/app.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/constants.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/error_info.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/logging/access_logger.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/logging/application_logger.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/logging/base_logger.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/logging/bootstrapper.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/logging/types.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/middlewares.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/modifiers/__init__.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/modifiers/lib.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/response_class.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/server.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/sources/__init__.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/sources/inline.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/sources/lib.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/sources/poller.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/static/sass/style.scss +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/static/style.css +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/statistics.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/templates/base.html +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/templates/err.html +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/templates/resources.html +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/templates/ul_filter.html +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/testing/modifiers.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/__init__.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/auth.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/crypto/__init__.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/crypto/crypto.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/crypto/suites/__init__.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/crypto/suites/base_cipher.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/crypto/suites/disabled_cipher.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/crypto/suites/fernet_cipher.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/dictupdate.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/eds.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/entry_point_loader.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/mock.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/resources.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/templates.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/timer.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/version_info.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/weighted_clusters.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/views/__init__.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/views/admin.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/views/crypto.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/views/discovery.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/views/healthchecks.py +0 -0
- {sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/views/interface.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "sovereign"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.29.0a3"
|
|
4
4
|
description = "Envoy Proxy control-plane written in Python"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
packages = [
|
|
@@ -41,6 +41,26 @@ sovereign = 'sovereign.server:main'
|
|
|
41
41
|
|
|
42
42
|
[tool.poetry.plugins."sovereign.loaders"]
|
|
43
43
|
"example" = "sovereign.testing.loaders:Multiply"
|
|
44
|
+
"file" = "sovereign.dynamic_config.loaders:File"
|
|
45
|
+
"pkgdata" = "sovereign.dynamic_config.loaders:PackageData"
|
|
46
|
+
"http" = "sovereign.dynamic_config.loaders:Web"
|
|
47
|
+
"https" = "sovereign.dynamic_config.loaders:Web"
|
|
48
|
+
"env" = "sovereign.dynamic_config.loaders:EnvironmentVariable"
|
|
49
|
+
"module" = "sovereign.dynamic_config.loaders:PythonModule"
|
|
50
|
+
"s3" = "sovereign.dynamic_config.loaders:S3Bucket"
|
|
51
|
+
"python" = "sovereign.dynamic_config.loaders:PythonInlineCode"
|
|
52
|
+
"inline" = "sovereign.dynamic_config.loaders:Inline"
|
|
53
|
+
|
|
54
|
+
[tool.poetry.plugins."sovereign.deserializers"]
|
|
55
|
+
"yaml" = "sovereign.dynamic_config.deser:YamlDeserializer"
|
|
56
|
+
"json" = "sovereign.dynamic_config.deser:JsonDeserializer"
|
|
57
|
+
"jinja" = "sovereign.dynamic_config.deser:JinjaDeserializer"
|
|
58
|
+
"jinja2" = "sovereign.dynamic_config.deser:JinjaDeserializer"
|
|
59
|
+
"string" = "sovereign.dynamic_config.deser:StringDeserializer"
|
|
60
|
+
"raw" = "sovereign.dynamic_config.deser:PassthroughDeserializer"
|
|
61
|
+
"ujson" = "sovereign.dynamic_config.deser:UjsonDeserializer"
|
|
62
|
+
"orjson" = "sovereign.dynamic_config.deser:OrjsonDeserializer"
|
|
63
|
+
|
|
44
64
|
|
|
45
65
|
[tool.poetry.dependencies]
|
|
46
66
|
python = "^3.11"
|
|
@@ -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
|
|
|
@@ -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
|
|
|
@@ -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:
|
|
@@ -23,6 +23,7 @@ except ImportError:
|
|
|
23
23
|
from sovereign import XDS_TEMPLATES, config, logs, template_context
|
|
24
24
|
from sovereign.utils.version_info import compute_hash
|
|
25
25
|
from sovereign.schemas import XdsTemplate, DiscoveryRequest, ProcessedTemplate
|
|
26
|
+
from sovereign.tracing import Tracer
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
try:
|
|
@@ -105,7 +106,8 @@ def response(request: DiscoveryRequest, xds_type: str) -> ProcessedTemplate:
|
|
|
105
106
|
resource_names=request.resources,
|
|
106
107
|
**template_context.get_context(request, template),
|
|
107
108
|
)
|
|
108
|
-
|
|
109
|
+
with Tracer("template rendering"):
|
|
110
|
+
content = template(**context)
|
|
109
111
|
|
|
110
112
|
# Deserialize YAML output from Jinja2
|
|
111
113
|
if not template.is_python_source:
|
|
@@ -116,13 +118,19 @@ def response(request: DiscoveryRequest, xds_type: str) -> ProcessedTemplate:
|
|
|
116
118
|
content = deserialize_config(content)
|
|
117
119
|
|
|
118
120
|
# Early return if the template is identical
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
with Tracer("nested test"):
|
|
122
|
+
with Tracer("hashing"):
|
|
123
|
+
config_version = compute_hash(content)
|
|
124
|
+
if (
|
|
125
|
+
config_version == request.version_info
|
|
126
|
+
and not config.discovery_cache.enabled
|
|
127
|
+
):
|
|
128
|
+
return ProcessedTemplate(version_info=config_version, resources=[])
|
|
129
|
+
|
|
130
|
+
if not isinstance(content, dict):
|
|
131
|
+
raise RuntimeError(f"Attempting to filter unstructured data: {content}")
|
|
132
|
+
with Tracer("filtering"):
|
|
133
|
+
resources = filter_resources(content["resources"], request.resources)
|
|
126
134
|
return ProcessedTemplate(resources=resources, version_info=config_version)
|
|
127
135
|
|
|
128
136
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Any, Dict, Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from sovereign.utils.entry_point_loader import EntryPointLoader
|
|
6
|
+
from sovereign.dynamic_config.loaders import CustomLoader
|
|
7
|
+
from sovereign.dynamic_config.deser import ConfigDeserializer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Loadable(BaseModel):
|
|
11
|
+
path: str
|
|
12
|
+
protocol: str
|
|
13
|
+
serialization: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
def load(self, default: Any = None) -> Any:
|
|
16
|
+
if self.protocol not in custom_loaders:
|
|
17
|
+
raise KeyError(
|
|
18
|
+
f"Could not find CustomLoader {self.protocol}. Available: {custom_loaders}"
|
|
19
|
+
)
|
|
20
|
+
loader_ = custom_loaders[self.protocol]
|
|
21
|
+
|
|
22
|
+
ser = self.serialization
|
|
23
|
+
if ser is None:
|
|
24
|
+
ser = loader_.default_deser
|
|
25
|
+
if ser not in deserializers:
|
|
26
|
+
raise KeyError(
|
|
27
|
+
f"Could not find Deserializer {ser}. Available: {deserializers}"
|
|
28
|
+
)
|
|
29
|
+
deserializer = deserializers[ser]
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
data = loader_.load(self.path)
|
|
33
|
+
return deserializer.deserialize(data)
|
|
34
|
+
except Exception as original_error:
|
|
35
|
+
if default is not None:
|
|
36
|
+
return default
|
|
37
|
+
raise Exception(
|
|
38
|
+
f"{self.protocol=}, {self.path=}, {self.serialization=}, {original_error=}"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def from_legacy_fmt(fmt_string: str) -> "Loadable":
|
|
43
|
+
if "://" not in fmt_string:
|
|
44
|
+
return Loadable(protocol="inline", serialization="string", path=fmt_string)
|
|
45
|
+
try:
|
|
46
|
+
scheme, path = fmt_string.split("://")
|
|
47
|
+
except ValueError:
|
|
48
|
+
raise ValueError(fmt_string)
|
|
49
|
+
try:
|
|
50
|
+
proto, ser = scheme.split("+")
|
|
51
|
+
except ValueError:
|
|
52
|
+
proto, ser = scheme, "yaml"
|
|
53
|
+
|
|
54
|
+
if proto in ("python", "module"):
|
|
55
|
+
ser = "raw"
|
|
56
|
+
if proto in ("http", "https"):
|
|
57
|
+
path = "://".join([proto, path])
|
|
58
|
+
|
|
59
|
+
return Loadable(
|
|
60
|
+
protocol=proto,
|
|
61
|
+
serialization=ser,
|
|
62
|
+
path=path,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
custom_loaders: Dict[str, CustomLoader] = {}
|
|
67
|
+
loader_entry_point = EntryPointLoader("loaders")
|
|
68
|
+
for entry_point in loader_entry_point.groups["loaders"]:
|
|
69
|
+
custom_loader = entry_point.load()
|
|
70
|
+
try:
|
|
71
|
+
func = custom_loader()
|
|
72
|
+
except AttributeError:
|
|
73
|
+
raise AttributeError("CustomLoader does not implement .load()")
|
|
74
|
+
custom_loaders[entry_point.name] = func
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
deserializers: Dict[str, ConfigDeserializer] = {}
|
|
78
|
+
deser_entry_point = EntryPointLoader("deserializers")
|
|
79
|
+
for entry_point in deser_entry_point.groups["deserializers"]:
|
|
80
|
+
deserializer = entry_point.load()
|
|
81
|
+
try:
|
|
82
|
+
func = deserializer()
|
|
83
|
+
except AttributeError:
|
|
84
|
+
raise AttributeError("Deserializer does not implement .deserialize()")
|
|
85
|
+
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
|
|
@@ -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
|
|
|
@@ -164,15 +165,15 @@ class XdsTemplate:
|
|
|
164
165
|
return self.code.render(*args, **kwargs)
|
|
165
166
|
|
|
166
167
|
def load_source(self) -> str:
|
|
167
|
-
|
|
168
|
+
old_serialization = self.loadable.serialization
|
|
169
|
+
if self.loadable.serialization in ("jinja", "jinja2"):
|
|
168
170
|
# The Jinja2 template serializer does not properly set a name
|
|
169
171
|
# for the loaded template.
|
|
170
172
|
# The repr for the template prints out as the memory address
|
|
171
173
|
# This makes it really hard to generate a consistent version_info string
|
|
172
174
|
# in rendered configuration.
|
|
173
175
|
# For this reason, we re-load the template as a string instead, and create a checksum.
|
|
174
|
-
|
|
175
|
-
self.loadable.serialization = Serialization("string")
|
|
176
|
+
self.loadable.serialization = "string"
|
|
176
177
|
ret = self.loadable.load()
|
|
177
178
|
self.loadable.serialization = old_serialization
|
|
178
179
|
return str(ret)
|
|
@@ -180,9 +181,8 @@ class XdsTemplate:
|
|
|
180
181
|
# If the template specified is a python source file,
|
|
181
182
|
# we can simply read and return the source of it.
|
|
182
183
|
old_protocol = self.loadable.protocol
|
|
183
|
-
old_serialization = self.loadable.serialization
|
|
184
184
|
self.loadable.protocol = "inline"
|
|
185
|
-
self.loadable.serialization =
|
|
185
|
+
self.loadable.serialization = "string"
|
|
186
186
|
ret = self.loadable.load()
|
|
187
187
|
self.loadable.protocol = old_protocol
|
|
188
188
|
self.loadable.serialization = old_serialization
|
|
@@ -458,6 +458,7 @@ class SovereignConfig(BaseSettings):
|
|
|
458
458
|
log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
|
|
459
459
|
ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
|
|
460
460
|
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
461
|
+
tracing: Optional["TracingConfig"] = None
|
|
461
462
|
model_config = SettingsConfigDict(
|
|
462
463
|
env_file=".env",
|
|
463
464
|
extra="ignore",
|
|
@@ -644,6 +645,46 @@ class SourcesConfiguration(BaseSettings):
|
|
|
644
645
|
)
|
|
645
646
|
|
|
646
647
|
|
|
648
|
+
class TracingConfig(BaseSettings):
|
|
649
|
+
enabled: bool = Field(False)
|
|
650
|
+
collector: str = Field("notset")
|
|
651
|
+
endpoint: str = Field("/v2/api/spans")
|
|
652
|
+
trace_id_128bit: bool = Field(True)
|
|
653
|
+
tags: Dict[str, Union[Loadable, str]] = dict()
|
|
654
|
+
model_config = SettingsConfigDict(
|
|
655
|
+
env_file=".env",
|
|
656
|
+
extra="ignore",
|
|
657
|
+
env_file_encoding="utf-8",
|
|
658
|
+
populate_by_name=True,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
@field_validator("tags", mode="before")
|
|
662
|
+
@classmethod
|
|
663
|
+
def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
|
|
664
|
+
ret = dict()
|
|
665
|
+
for key, value in v.items():
|
|
666
|
+
if isinstance(value, dict):
|
|
667
|
+
ret[key] = Loadable(**value).load()
|
|
668
|
+
elif isinstance(value, str):
|
|
669
|
+
ret[key] = Loadable.from_legacy_fmt(value).load()
|
|
670
|
+
else:
|
|
671
|
+
raise ValueError(f"Received an invalid tag for tracing: {value}")
|
|
672
|
+
return ret
|
|
673
|
+
|
|
674
|
+
@model_validator(mode="before")
|
|
675
|
+
@classmethod
|
|
676
|
+
def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
|
|
677
|
+
if enabled := getenv("SOVEREIGN_TRACING_ENABLED"):
|
|
678
|
+
values["enabled"] = enabled
|
|
679
|
+
if collector := getenv("SOVEREIGN_TRACING_COLLECTOR"):
|
|
680
|
+
values["collector"] = collector
|
|
681
|
+
if endpoint := getenv("SOVEREIGN_TRACING_ENDPOINT"):
|
|
682
|
+
values["endpoint"] = endpoint
|
|
683
|
+
if trace_id_128bit := getenv("SOVEREIGN_TRACING_TRACE_ID_128BIT"):
|
|
684
|
+
values["trace_id_128bit"] = trace_id_128bit
|
|
685
|
+
return values
|
|
686
|
+
|
|
687
|
+
|
|
647
688
|
class LegacyConfig(BaseSettings):
|
|
648
689
|
regions: Optional[List[str]] = None
|
|
649
690
|
eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
|
|
@@ -731,6 +772,7 @@ class SovereignConfigv2(BaseSettings):
|
|
|
731
772
|
debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
|
|
732
773
|
legacy_fields: LegacyConfig = LegacyConfig()
|
|
733
774
|
discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
|
|
775
|
+
tracing: TracingConfig = TracingConfig()
|
|
734
776
|
model_config = SettingsConfigDict(
|
|
735
777
|
env_file=".env",
|
|
736
778
|
extra="ignore",
|
|
@@ -821,6 +863,7 @@ class SovereignConfigv2(BaseSettings):
|
|
|
821
863
|
statsd=other.statsd,
|
|
822
864
|
sentry_dsn=SecretStr(other.sentry_dsn),
|
|
823
865
|
debug=other.debug_enabled,
|
|
866
|
+
tracing=other.tracing,
|
|
824
867
|
legacy_fields=LegacyConfig(
|
|
825
868
|
regions=other.regions,
|
|
826
869
|
eds_priority_matrix=other.eds_priority_matrix,
|
|
@@ -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}")
|
|
@@ -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
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sovereign-0.28.0 → sovereign-0.29.0a3}/src/sovereign/utils/crypto/suites/disabled_cipher.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|