sovereign 0.28.0__py3-none-any.whl → 0.29.0a3__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
@@ -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
- content = template(**context)
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
- config_version = compute_hash(content)
120
- if config_version == request.version_info and not config.discovery_cache.enabled:
121
- return ProcessedTemplate(version_info=config_version, resources=[])
122
-
123
- if not isinstance(content, dict):
124
- raise RuntimeError(f"Attempting to filter unstructured data: {content}")
125
- resources = filter_resources(content["resources"], request.resources)
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
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
 
@@ -164,15 +165,15 @@ class XdsTemplate:
164
165
  return self.code.render(*args, **kwargs)
165
166
 
166
167
  def load_source(self) -> str:
167
- if self.loadable.serialization in (Serialization.jinja, Serialization.jinja2):
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
- old_serialization = self.loadable.serialization
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 = Serialization("string")
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,
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):
@@ -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}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 0.28.0
3
+ Version: 0.29.0a3
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
@@ -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=KHnWXUlceUglbR9eV5Q6LjPOpGv2dcrGOSus9-QLQJM,5952
5
+ sovereign/context.py,sha256=Vwr-Jmo1I_05rFraxv6GoYEC83oFl87LG16px4R7IfA,6461
6
+ sovereign/discovery.py,sha256=iS34aeJHSTqGY4i0jIQG9Yd0LKS63VQELaMSyvJk6Y8,6198
7
+ sovereign/dynamic_config/__init__.py,sha256=kxthEPcB-fiVZ-5qudkct5bphsAo3LuGAE7rU0J9M7Q,2781
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=q-Y6xX3YxOM2ADcuafpK0bCgHpGcOQPgUPbLKwqWnCc,29963
20
+ sovereign/schemas.py,sha256=wikM9mt42lbbxKEC60N8emG8fLrwnDp9ikkbJMECGXQ,31516
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
@@ -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=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
@@ -55,8 +58,8 @@ sovereign/views/crypto.py,sha256=o8NSyiUBy7v1pMOXt_1UBi68FNcGkXSlEVg9C18y8kY,332
55
58
  sovereign/views/discovery.py,sha256=TVvWTMzWydsC-SNKL9WsSss_Hfnt2Ed4SVC2A8Na7Jo,5932
56
59
  sovereign/views/healthchecks.py,sha256=_WkMunlrFpqGTLgtNtRr7gCsDCv5kiuYxCyTi-dMEKM,1357
57
60
  sovereign/views/interface.py,sha256=TFXbYp5oXZPRkVnAo-NWQFBb8XMtB519FaV78ludCcI,7055
58
- sovereign-0.28.0.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
59
- sovereign-0.28.0.dist-info/METADATA,sha256=R6mLEGv_aFECE_dYbIaKBzfa0CbD6E1a7WBiqbJZFx4,6556
60
- sovereign-0.28.0.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
61
- sovereign-0.28.0.dist-info/entry_points.txt,sha256=T6HkcZHqKSDcK9d23svUU-vt-_DQbSV0SRt4_UtQGEc,289
62
- sovereign-0.28.0.dist-info/RECORD,,
61
+ sovereign-0.29.0a3.dist-info/LICENSE.txt,sha256=2X125zvAb9AYLjCgdMDQZuufhm0kwcg31A8pGKj_-VY,560
62
+ sovereign-0.29.0a3.dist-info/METADATA,sha256=pvrbBMc76iClWyj0-xIP5vcy0T6XTmzBYNCHF9qeEEg,6558
63
+ sovereign-0.29.0a3.dist-info/WHEEL,sha256=d2fvjOD7sXsVzChCqf0Ty0JbHKBaLYwDbGQDwQTnJ50,88
64
+ sovereign-0.29.0a3.dist-info/entry_points.txt,sha256=2mUHQjqeXEokMF6ZjDmvqQ9Fxk-Or2S4eC0h70ZxKmk,1201
65
+ sovereign-0.29.0a3.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
+
@@ -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
-