sovereign 0.25.4__tar.gz → 0.27.0__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.

Files changed (61) hide show
  1. {sovereign-0.25.4 → sovereign-0.27.0}/PKG-INFO +4 -2
  2. {sovereign-0.25.4 → sovereign-0.27.0}/pyproject.toml +14 -3
  3. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/__init__.py +1 -1
  4. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/app.py +4 -2
  5. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/config_loader.py +45 -46
  6. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/configuration.py +1 -1
  7. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/context.py +32 -26
  8. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/schemas.py +187 -190
  9. sovereign-0.27.0/src/sovereign/testing/loaders.py +9 -0
  10. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/crypto.py +2 -0
  11. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/admin.py +2 -2
  12. {sovereign-0.25.4 → sovereign-0.27.0}/LICENSE.txt +0 -0
  13. {sovereign-0.25.4 → sovereign-0.27.0}/README.md +0 -0
  14. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/constants.py +0 -0
  15. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/discovery.py +0 -0
  16. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/error_info.py +0 -0
  17. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/access_logger.py +0 -0
  18. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/application_logger.py +0 -0
  19. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/base_logger.py +0 -0
  20. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/bootstrapper.py +0 -0
  21. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/logging/types.py +0 -0
  22. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/middlewares.py +0 -0
  23. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/modifiers/__init__.py +0 -0
  24. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/modifiers/lib.py +0 -0
  25. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/response_class.py +0 -0
  26. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/server.py +0 -0
  27. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/__init__.py +0 -0
  28. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/file.py +0 -0
  29. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/inline.py +0 -0
  30. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/lib.py +0 -0
  31. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/sources/poller.py +0 -0
  32. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/static/sass/style.scss +0 -0
  33. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/static/style.css +0 -0
  34. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/statistics.py +0 -0
  35. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/templates/base.html +0 -0
  36. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/templates/err.html +0 -0
  37. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/templates/resources.html +0 -0
  38. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/templates/ul_filter.html +0 -0
  39. /sovereign-0.25.4/src/sovereign/modifiers/test.py → /sovereign-0.27.0/src/sovereign/testing/modifiers.py +0 -0
  40. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/__init__.py +0 -0
  41. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/auth.py +0 -0
  42. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/__init__.py +0 -0
  43. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/__init__.py +0 -0
  44. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/aes_gcm_cipher.py +0 -0
  45. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/base_cipher.py +0 -0
  46. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/disabled_cipher.py +0 -0
  47. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/crypto/suites/fernet_cipher.py +0 -0
  48. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/dictupdate.py +0 -0
  49. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/eds.py +0 -0
  50. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/entry_point_loader.py +0 -0
  51. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/mock.py +0 -0
  52. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/resources.py +0 -0
  53. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/templates.py +0 -0
  54. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/timer.py +0 -0
  55. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/version_info.py +0 -0
  56. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/utils/weighted_clusters.py +0 -0
  57. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/__init__.py +0 -0
  58. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/crypto.py +0 -0
  59. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/discovery.py +0 -0
  60. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/healthchecks.py +0 -0
  61. {sovereign-0.25.4 → sovereign-0.27.0}/src/sovereign/views/interface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: sovereign
3
- Version: 0.25.4
3
+ Version: 0.27.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
@@ -44,13 +44,15 @@ Requires-Dist: glom (>=23.3.0,<24.0.0)
44
44
  Requires-Dist: gunicorn (>=22.0.0,<23.0.0)
45
45
  Requires-Dist: httptools (>=0.6.0,<0.7.0) ; extra == "httptools"
46
46
  Requires-Dist: orjson (>=3.9.15,<4.0.0) ; extra == "orjson"
47
+ Requires-Dist: pydantic (>=2.7.2,<3.0.0)
48
+ Requires-Dist: pydantic-settings (>=2.3.1,<3.0.0)
47
49
  Requires-Dist: redis (<=5.0.0)
48
50
  Requires-Dist: requests (>=2.31.0,<3.0.0)
49
51
  Requires-Dist: sentry-sdk (>=1.23.1,<2.0.0) ; extra == "sentry"
50
52
  Requires-Dist: structlog (>=23.1.0,<24.0.0)
51
53
  Requires-Dist: ujson (>=5.8.0,<6.0.0) ; extra == "ujson"
52
54
  Requires-Dist: uvicorn (>=0.23.2,<0.24.0)
53
- Requires-Dist: uvloop (>=0.17.0,<0.18.0)
55
+ Requires-Dist: uvloop (>=0.19.0,<0.20.0)
54
56
  Project-URL: Documentation, https://vsyrakis.bitbucket.io/sovereign/docs/
55
57
  Project-URL: Repository, https://bitbucket.org/atlassian/sovereign/src/master/
56
58
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "sovereign"
3
- version = "0.25.4"
3
+ version = "0.27.0"
4
4
  description = "Envoy Proxy control-plane written in Python"
5
5
  license = "Apache-2.0"
6
6
  packages = [
@@ -37,7 +37,10 @@ sovereign = 'sovereign.server:main'
37
37
  "inline" = "sovereign.sources.inline:Inline"
38
38
 
39
39
  [tool.poetry.plugins."sovereign.modifiers"]
40
- "sovereign_3rd_party_test" = "sovereign.modifiers.test:Test"
40
+ "sovereign_3rd_party_test" = "sovereign.testing.modifiers:Test"
41
+
42
+ [tool.poetry.plugins."sovereign.loaders"]
43
+ "example" = "sovereign.testing.loaders:Multiply"
41
44
 
42
45
  [tool.poetry.dependencies]
43
46
  python = "^3.11"
@@ -52,7 +55,7 @@ cachelib = "^0.10.2"
52
55
  glom = "^23.3.0"
53
56
  cryptography = "^42.0.5"
54
57
  fastapi = "^0.110.0"
55
- uvloop = "^0.17.0"
58
+ uvloop = "^0.19.0"
56
59
  sentry-sdk = "^1.23.1"
57
60
  boto3 = {version = "^1.28.62", optional = true}
58
61
  datadog = {version = "^0.47.0", optional = true}
@@ -63,6 +66,8 @@ cashews = {extras = ["redis"], version = "^6.3.0", optional = true}
63
66
  redis = {version = "<= 5.0.0", optional = true}
64
67
  httptools = {version = "^0.6.0", optional = true}
65
68
  cachetools = "^5.3.2"
69
+ pydantic = "^2.7.2"
70
+ pydantic-settings = "^2.3.1"
66
71
 
67
72
  [tool.poetry.extras]
68
73
  sentry = ["sentry-sdk"]
@@ -111,6 +116,12 @@ lint = { cmd = "pylint src/sovereign", help = "Run linter checks" }
111
116
  [tool.black]
112
117
  target-version = ['py311']
113
118
 
119
+ [tool.mypy]
120
+ plugins = [
121
+ "pydantic.mypy"
122
+ ]
123
+ ignore_missing_imports = true
124
+
114
125
  [tool.coverage.run]
115
126
  omit = ["test/*"]
116
127
 
@@ -4,7 +4,7 @@ from importlib.metadata import version
4
4
  from typing import Any, Mapping, Type
5
5
 
6
6
  from fastapi.responses import JSONResponse
7
- from pydantic.error_wrappers import ValidationError
7
+ from pydantic import ValidationError
8
8
  from starlette.templating import Jinja2Templates
9
9
 
10
10
  from sovereign import config_loader
@@ -1,9 +1,11 @@
1
1
  import asyncio
2
2
  import traceback
3
- import uvicorn
4
3
  from collections import namedtuple
4
+
5
+ import uvicorn
5
6
  from fastapi import FastAPI, Request
6
7
  from fastapi.responses import RedirectResponse, FileResponse, Response, JSONResponse
8
+
7
9
  from sovereign import (
8
10
  __version__,
9
11
  config,
@@ -14,12 +16,12 @@ from sovereign import (
14
16
  logs,
15
17
  )
16
18
  from sovereign.error_info import ErrorInfo
19
+ from sovereign.utils.resources import get_package_file
17
20
  from sovereign.views import crypto, discovery, healthchecks, admin, interface
18
21
  from sovereign.middlewares import (
19
22
  RequestContextLogMiddleware,
20
23
  LoggingMiddleware,
21
24
  )
22
- from sovereign.utils.resources import get_package_file
23
25
 
24
26
  Router = namedtuple("Router", "module tags prefix")
25
27
 
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  import json
3
3
  from enum import Enum
4
- from typing import Any, Dict, Callable, Union
4
+ from typing import Any, Dict, Callable, Union, Protocol
5
5
  from types import ModuleType
6
6
  import yaml
7
7
  import jinja2
@@ -14,6 +14,11 @@ from sovereign.utils.resources import get_package_file_bytes
14
14
 
15
15
 
16
16
  class Serialization(Enum):
17
+ """
18
+ Types of deserialization available in Sovereign
19
+ for loading configuration field values.
20
+ """
21
+
17
22
  yaml = "yaml"
18
23
  json = "json"
19
24
  orjson = "orjson"
@@ -22,18 +27,7 @@ class Serialization(Enum):
22
27
  jinja2 = "jinja2"
23
28
  string = "string"
24
29
  raw = "raw"
25
-
26
-
27
- class Protocol(Enum):
28
- file = "file"
29
- http = "http"
30
- https = "https"
31
- pkgdata = "pkgdata"
32
- env = "env"
33
- module = "module"
34
- s3 = "s3"
35
- python = "python"
36
- inline = "inline"
30
+ skip = "skip"
37
31
 
38
32
 
39
33
  jinja_env = jinja2.Environment(autoescape=True)
@@ -56,6 +50,7 @@ serializers: Dict[Serialization, Callable[[Any], Any]] = {
56
50
  Serialization.raw: passthrough,
57
51
  }
58
52
 
53
+
59
54
  try:
60
55
  import ujson
61
56
 
@@ -99,8 +94,13 @@ except ImportError:
99
94
  boto3 = None
100
95
 
101
96
 
97
+ class CustomLoader(Protocol):
98
+ def load(self, path: str, ser: Serialization) -> Any:
99
+ ...
100
+
101
+
102
102
  class Loadable(BaseModel):
103
- protocol: Protocol = Protocol.http
103
+ protocol: str = "http"
104
104
  serialization: Serialization = Serialization.yaml
105
105
  path: str
106
106
 
@@ -113,26 +113,25 @@ class Loadable(BaseModel):
113
113
  raise
114
114
 
115
115
  @staticmethod
116
- def from_legacy_fmt(s: str) -> "Loadable":
117
- if "://" not in s:
116
+ def from_legacy_fmt(fmt_string: str) -> "Loadable":
117
+ if "://" not in fmt_string:
118
118
  return Loadable(
119
- protocol=Protocol.inline, serialization=Serialization.string, path=s
119
+ protocol="inline", serialization=Serialization.string, path=fmt_string
120
120
  )
121
121
  try:
122
- scheme, path = s.split("://")
122
+ scheme, path = fmt_string.split("://")
123
123
  except ValueError:
124
- raise ValueError(s)
124
+ raise ValueError(fmt_string)
125
125
  try:
126
- p, s = scheme.split("+")
126
+ proto, ser = scheme.split("+")
127
127
  except ValueError:
128
- p, s = scheme, "yaml"
128
+ proto, ser = scheme, "yaml"
129
129
 
130
- proto: Protocol = Protocol(p)
131
- serialization: Serialization = Serialization(s)
132
- if proto in (Protocol.python, Protocol.module):
130
+ serialization: Serialization = Serialization(ser)
131
+ if proto in ("python", "module"):
133
132
  serialization = Serialization.raw
134
- if proto in (Protocol.http, Protocol.https):
135
- path = "://".join([proto.value, path])
133
+ if proto in ("http", "https"):
134
+ path = "://".join([proto, path])
136
135
 
137
136
  return Loadable(
138
137
  protocol=proto,
@@ -145,32 +144,32 @@ def raise_(e: Exception) -> Exception:
145
144
  raise e
146
145
 
147
146
 
148
- def load_file(path: str, loader: Serialization) -> Any:
147
+ def load_file(path: str, ser: Serialization) -> Any:
149
148
  with open(path) as f:
150
149
  contents = f.read()
151
150
  try:
152
- return serializers[loader](contents)
151
+ return serializers[ser](contents)
153
152
  except FileNotFoundError:
154
153
  raise FileNotFoundError(f"Unable to load {path}")
155
154
 
156
155
 
157
- def load_package_data(path: str, loader: Serialization) -> Any:
156
+ def load_package_data(path: str, ser: Serialization) -> Any:
158
157
  pkg, pkg_file = path.split(":")
159
158
  data = get_package_file_bytes(pkg, pkg_file)
160
- return serializers[loader](data)
159
+ return serializers[ser](data)
161
160
 
162
161
 
163
- def load_http(path: str, loader: Serialization) -> Any:
162
+ def load_http(path: str, ser: Serialization) -> Any:
164
163
  response = requests.get(path)
165
164
  response.raise_for_status()
166
165
  data = response.text
167
- return serializers[loader](data)
166
+ return serializers[ser](data)
168
167
 
169
168
 
170
- def load_env(variable: str, loader: Serialization = Serialization.raw) -> Any:
169
+ def load_env(variable: str, ser: Serialization = Serialization.raw) -> Any:
171
170
  data = os.getenv(variable)
172
171
  try:
173
- return serializers[loader](data)
172
+ return serializers[ser](data)
174
173
  except AttributeError as e:
175
174
  raise AttributeError(
176
175
  f"Unable to read environment variable {variable}: {repr(e)}"
@@ -188,7 +187,7 @@ def load_module(name: str, _: Serialization = Serialization.raw) -> Any:
188
187
  return imported
189
188
 
190
189
 
191
- def load_s3(path: str, loader: Serialization = Serialization.raw) -> Any:
190
+ def load_s3(path: str, ser: Serialization = Serialization.raw) -> Any:
192
191
  if isinstance(boto3, type(None)):
193
192
  raise ImportError(
194
193
  "boto3 must be installed to load S3 paths. Use ``pip install sovereign[boto]``"
@@ -197,7 +196,7 @@ def load_s3(path: str, loader: Serialization = Serialization.raw) -> Any:
197
196
  s3 = boto3.client("s3")
198
197
  response = s3.get_object(Bucket=bucket, Key=key)
199
198
  data = "".join([chunk.decode() for chunk in response["Body"]])
200
- return serializers[loader](data)
199
+ return serializers[ser](data)
201
200
 
202
201
 
203
202
  def load_python(path: str, _: Serialization = Serialization.raw) -> ModuleType:
@@ -210,14 +209,14 @@ def load_inline(path: str, _: Serialization = Serialization.raw) -> Any:
210
209
  return str(path)
211
210
 
212
211
 
213
- loaders: Dict[Protocol, Callable[[str, Serialization], Union[str, Any]]] = {
214
- Protocol.file: load_file,
215
- Protocol.pkgdata: load_package_data,
216
- Protocol.http: load_http,
217
- Protocol.https: load_http,
218
- Protocol.env: load_env,
219
- Protocol.module: load_module,
220
- Protocol.s3: load_s3,
221
- Protocol.python: load_python,
222
- Protocol.inline: load_inline,
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,
223
222
  }
@@ -1,7 +1,7 @@
1
1
  import os
2
2
  from typing import Any, Mapping
3
3
 
4
- from pydantic.error_wrappers import ValidationError
4
+ from pydantic import ValidationError
5
5
 
6
6
  from sovereign import config_loader
7
7
  from sovereign.context import TemplateContext
@@ -13,6 +13,8 @@ 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
16
18
  from structlog.stdlib import BoundLogger
17
19
 
18
20
  from sovereign.config_loader import Loadable
@@ -53,6 +55,14 @@ class TemplateContext:
53
55
  self.stats = stats
54
56
  # initial load
55
57
  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
56
66
  asyncio.run(self.load_context_variables())
57
67
 
58
68
  async def start_refresh_context(self) -> NoReturn:
@@ -60,7 +70,6 @@ class TemplateContext:
60
70
  await poll_forever_cron(self.refresh_cron, self.refresh_context)
61
71
  elif self.refresh_rate is not None:
62
72
  await poll_forever(self.refresh_rate, self.refresh_context)
63
-
64
73
  raise RuntimeError("Failed to start refresh_context, this should never happen")
65
74
 
66
75
  async def refresh_context(self) -> None:
@@ -68,25 +77,25 @@ class TemplateContext:
68
77
 
69
78
  async def _load_context(
70
79
  self,
71
- context_name: str,
72
- context_config: Loadable | str,
80
+ name: str,
81
+ loader: Loadable | str,
73
82
  refresh_num_retries: int,
74
83
  refresh_retry_interval_secs: int,
75
84
  ) -> LoadContextResponse:
76
85
  retries_left = refresh_num_retries
77
- context_response = {}
86
+ response = {}
78
87
 
79
88
  while True:
80
89
  try:
81
- if isinstance(context_config, Loadable):
82
- context_response = context_config.load()
83
- elif isinstance(context_config, str):
84
- context_response = Loadable.from_legacy_fmt(context_config).load()
90
+ if isinstance(loader, Loadable):
91
+ response = loader.load()
92
+ elif isinstance(loader, str):
93
+ response = Loadable.from_legacy_fmt(loader).load()
85
94
  self.stats.increment(
86
95
  "context.refresh.success",
87
- tags=[f"context:{context_name}"],
96
+ tags=[f"context:{name}"],
88
97
  )
89
- return LoadContextResponse(context_name, context_response)
98
+ return LoadContextResponse(name, response)
90
99
  # pylint: disable=broad-except
91
100
  except Exception as e:
92
101
  retries_left -= 1
@@ -95,34 +104,29 @@ class TemplateContext:
95
104
  self.logger.error(str(e), traceback=tb)
96
105
  self.stats.increment(
97
106
  "context.refresh.error",
98
- tags=[f"context:{context_name}"],
107
+ tags=[f"context:{name}"],
99
108
  )
100
- return LoadContextResponse(context_name, context_response, False)
109
+ return LoadContextResponse(name, response, False)
101
110
  else:
102
111
  await asyncio.sleep(refresh_retry_interval_secs)
103
112
 
104
113
  async def load_context_variables(self) -> None:
105
- context_coroutines: list[Awaitable[LoadContextResponse]] = []
106
- for context_name, context_config in self.configured_context.items():
107
- context_coroutines.append(
114
+ coroutines: list[Awaitable[LoadContextResponse]] = []
115
+ for name, conf in self.configured_context.items():
116
+ coroutines.append(
108
117
  self._load_context(
109
- context_name,
110
- context_config,
118
+ name,
119
+ conf,
111
120
  self.refresh_num_retries,
112
121
  self.refresh_retry_interval_secs,
113
122
  )
114
123
  )
115
124
 
116
- context_results: list[LoadContextResponse] = await asyncio.gather(
117
- *context_coroutines
118
- )
125
+ results: list[LoadContextResponse] = await asyncio.gather(*coroutines)
119
126
 
120
- for context_result in context_results:
121
- if (
122
- context_result.success
123
- or context_result.context_name not in self.context
124
- ):
125
- self.context[context_result.context_name] = context_result.context
127
+ for res in results:
128
+ if res.success or res.context_name not in self.context:
129
+ self.context[res.context_name] = res.context
126
130
 
127
131
  if "crypto" not in self.context and self.crypto:
128
132
  self.context["crypto"] = self.crypto
@@ -162,7 +166,9 @@ class TemplateContext:
162
166
  ret = self.build_new_context_from_instances(
163
167
  node_value=self.poller.extract_node_key(request.node),
164
168
  )
169
+ ret["__hide_from_ui"] = lambda v: v
165
170
  if request.hide_private_keys:
171
+ ret["__hide_from_ui"] = lambda _: "(value hidden)"
166
172
  ret["crypto"] = CipherContainer.from_encryption_configs(
167
173
  encryption_configs=[EncryptionConfig("", EncryptionType.DISABLED)],
168
174
  logger=self.logger,
@@ -5,21 +5,22 @@ from dataclasses import dataclass
5
5
  from enum import Enum
6
6
  from os import getenv
7
7
  from types import ModuleType
8
- from typing import Any, Dict, List, Optional, Tuple, Type, Union
8
+ from typing import Any, Dict, List, Optional, Self, Tuple, Type, Union
9
9
 
10
10
  from croniter import CroniterBadCronError, croniter
11
11
  from fastapi.responses import JSONResponse
12
12
  from jinja2 import Template, meta
13
13
  from pydantic import (
14
14
  BaseModel,
15
- BaseSettings,
15
+ ConfigDict,
16
16
  Field,
17
17
  SecretStr,
18
- root_validator,
19
- validator,
18
+ model_validator,
19
+ field_validator,
20
20
  )
21
+ from pydantic_settings import BaseSettings, SettingsConfigDict
21
22
 
22
- from sovereign.config_loader import Loadable, Protocol, Serialization, jinja_env
23
+ from sovereign.config_loader import Loadable, Serialization, jinja_env
23
24
  from sovereign.utils.crypto.suites import EncryptionType
24
25
  from sovereign.utils.version_info import compute_hash
25
26
 
@@ -65,11 +66,13 @@ class StatsdConfig(BaseModel):
65
66
  enabled: bool = False
66
67
  use_ms: bool = True
67
68
 
68
- @validator("host", pre=True)
69
+ @field_validator("host", mode="before")
70
+ @classmethod
69
71
  def load_host(cls, v: str) -> Any:
70
72
  return Loadable.from_legacy_fmt(v).load()
71
73
 
72
- @validator("port", pre=True)
74
+ @field_validator("port", mode="before")
75
+ @classmethod
73
76
  def load_port(cls, v: Union[int, str]) -> Any:
74
77
  if isinstance(v, int):
75
78
  return v
@@ -78,7 +81,8 @@ class StatsdConfig(BaseModel):
78
81
  else:
79
82
  raise ValueError(f"Received an invalid port: {v}")
80
83
 
81
- @validator("tags", pre=True)
84
+ @field_validator("tags", mode="before")
85
+ @classmethod
82
86
  def load_tags(cls, v: Dict[str, Union[Loadable, str]]) -> Dict[str, Any]:
83
87
  ret = dict()
84
88
  for key, value in v.items():
@@ -108,14 +112,14 @@ class DiscoveryCacheConfig(BaseModel):
108
112
  socket_keepalive: bool = True # Try to keep connections to redis around.
109
113
  ttl: int = 60
110
114
 
111
- @root_validator
112
- def set_default_protocol(cls, values: Dict[str, Any]) -> Dict[str, Any]:
113
- secure = values.get("secure")
114
- if secure:
115
- values["protocol"] = "rediss://"
116
- return values
115
+ @model_validator(mode="after")
116
+ def set_default_protocol(self) -> Self:
117
+ if self.secure:
118
+ self.protocol = "rediss://"
119
+ return self
117
120
 
118
- @root_validator
121
+ @model_validator(mode="before")
122
+ @classmethod
119
123
  def set_environmental_variables(cls, values: Dict[str, Any]) -> Dict[str, Any]:
120
124
  if host := getenv("SOVEREIGN_DISCOVERY_CACHE_REDIS_HOST"):
121
125
  values["host"] = host
@@ -132,7 +136,7 @@ class XdsTemplate:
132
136
  self.loadable: Loadable = Loadable.from_legacy_fmt(path)
133
137
  elif isinstance(path, Loadable):
134
138
  self.loadable = path
135
- self.is_python_source = self.loadable.protocol == Protocol.python
139
+ self.is_python_source = self.loadable.protocol == "python"
136
140
  self.source = self.load_source()
137
141
  template_ast = jinja_env.parse(self.source)
138
142
  self.jinja_variables = meta.find_undeclared_variables(template_ast)
@@ -177,7 +181,7 @@ class XdsTemplate:
177
181
  # we can simply read and return the source of it.
178
182
  old_protocol = self.loadable.protocol
179
183
  old_serialization = self.loadable.serialization
180
- self.loadable.protocol = Protocol("inline")
184
+ self.loadable.protocol = "inline"
181
185
  self.loadable.serialization = Serialization("string")
182
186
  ret = self.loadable.load()
183
187
  self.loadable.protocol = old_protocol
@@ -221,9 +225,9 @@ class ProcessedTemplate:
221
225
 
222
226
 
223
227
  class Locality(BaseModel):
224
- region: str = Field(None)
225
- zone: str = Field(None)
226
- sub_zone: str = Field(None)
228
+ region: Optional[str] = Field(None)
229
+ zone: Optional[str] = Field(None)
230
+ sub_zone: Optional[str] = Field(None)
227
231
 
228
232
 
229
233
  class SemanticVersion(BaseModel):
@@ -255,8 +259,8 @@ class Node(BaseModel):
255
259
  description="The ``--service-cluster`` configured by the Envoy client",
256
260
  )
257
261
  metadata: Dict[str, Any] = Field(default_factory=dict, title="Key:value metadata")
258
- locality: Locality = Field(Locality(), title="Locality") # type: ignore
259
- build_version: str = Field(
262
+ locality: Locality = Field(Locality(), title="Locality")
263
+ build_version: Optional[str] = Field(
260
264
  None, # Optional in the v3 Envoy API
261
265
  title="Envoy build/release version string",
262
266
  description="Used to identify what version of Envoy the "
@@ -269,7 +273,7 @@ class Node(BaseModel):
269
273
  client_features: List[str] = []
270
274
 
271
275
  @property
272
- def common(self) -> Tuple[str, str, str, BuildVersion, Locality]:
276
+ def common(self) -> Tuple[str, Optional[str], str, BuildVersion, Locality]:
273
277
  """
274
278
  Returns fields that are the same in adjacent proxies
275
279
  ie. proxies that are part of the same logical group
@@ -292,7 +296,7 @@ class Resources(List[str]):
292
296
  def __contains__(self, item: object) -> bool:
293
297
  if len(self) == 0:
294
298
  return True
295
- return item in list(self)
299
+ return super().__contains__(item)
296
300
 
297
301
 
298
302
  class Status(BaseModel):
@@ -306,19 +310,20 @@ class DiscoveryRequest(BaseModel):
306
310
  version_info: str = Field(
307
311
  "0", title="The version of the envoy clients current configuration"
308
312
  )
309
- resource_names: Resources = Field(
313
+ resource_names: list[str] | Resources = Field(
310
314
  Resources(), title="List of requested resource names"
311
315
  )
312
316
  hide_private_keys: bool = False
313
317
  type_url: Optional[str] = Field(
314
318
  None, title="The corresponding type_url for the requested resource"
315
319
  )
316
- desired_controlplane: str = Field(
320
+ desired_controlplane: Optional[str] = Field(
317
321
  None, title="The host header provided in the Discovery Request"
318
322
  )
319
- error_detail: Status = Field(
323
+ error_detail: Optional[Status] = Field(
320
324
  None, title="Error details from the previous xDS request"
321
325
  )
326
+ model_config = ConfigDict(arbitrary_types_allowed=True)
322
327
 
323
328
  @property
324
329
  def envoy_version(self) -> str:
@@ -328,6 +333,8 @@ class DiscoveryRequest(BaseModel):
328
333
  except AssertionError:
329
334
  try:
330
335
  build_version = self.node.build_version
336
+ if build_version is None:
337
+ return "default"
331
338
  _, version, *_ = build_version.split("/")
332
339
  except (AttributeError, ValueError):
333
340
  # TODO: log/metric this?
@@ -355,33 +362,35 @@ class DiscoveryResponse(BaseModel):
355
362
 
356
363
 
357
364
  class SovereignAsgiConfig(BaseSettings):
358
- host: str = "0.0.0.0"
359
- port: int = 8080
360
- keepalive: int = 5
361
- workers: int = multiprocessing.cpu_count() * 2 + 1
362
- threads: int = 1
365
+ host: str = Field("0.0.0.0", alias="SOVEREIGN_HOST")
366
+ port: int = Field(8080, alias="SOVEREIGN_PORT")
367
+ keepalive: int = Field(5, alias="SOVEREIGN_KEEPALIVE")
368
+ workers: int = Field(
369
+ default_factory=lambda: multiprocessing.cpu_count() * 2 + 1,
370
+ alias="SOVEREIGN_WORKERS",
371
+ )
372
+ threads: int = Field(1, alias="SOVEREIGN_THREADS")
363
373
  reuse_port: bool = True
364
- preload_app: bool = True
374
+ preload_app: bool = Field(True, alias="SOVEREIGN_PRELOAD")
365
375
  log_level: str = "warning"
366
376
  worker_class: str = "uvicorn.workers.UvicornWorker"
367
- worker_timeout: int = 30
377
+ worker_timeout: int = Field(30, alias="SOVEREIGN_WORKER_TIMEOUT")
368
378
  worker_tmp_dir: str = "/dev/shm"
369
- graceful_timeout: int = worker_timeout * 2
370
- max_requests: int = 0
371
- max_requests_jitter: int = 0
372
-
373
- class Config:
374
- fields = {
375
- "host": {"env": "SOVEREIGN_HOST"},
376
- "port": {"env": "SOVEREIGN_PORT"},
377
- "keepalive": {"env": "SOVEREIGN_KEEPALIVE"},
378
- "workers": {"env": "SOVEREIGN_WORKERS"},
379
- "threads": {"env": "SOVEREIGN_THREADS"},
380
- "preload_app": {"env": "SOVEREIGN_PRELOAD"},
381
- "worker_timeout": {"env": "SOVEREIGN_WORKER_TIMEOUT"},
382
- "max_requests": {"env": "SOVEREIGN_MAX_REQUESTS"},
383
- "max_requests_jitter": {"env": "SOVEREIGN_MAX_REQUESTS_JITTER"},
384
- }
379
+ graceful_timeout: Optional[int] = Field(None)
380
+ max_requests: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS")
381
+ max_requests_jitter: int = Field(0, alias="SOVEREIGN_MAX_REQUESTS_JITTER")
382
+ model_config = SettingsConfigDict(
383
+ env_file=".env",
384
+ extra="ignore",
385
+ env_file_encoding="utf-8",
386
+ populate_by_name=True,
387
+ )
388
+
389
+ @model_validator(mode="after")
390
+ def validate_graceful_timeout(self) -> Self:
391
+ if self.graceful_timeout is None:
392
+ self.graceful_timeout = self.worker_timeout * 2
393
+ return self
385
394
 
386
395
  def as_gunicorn_conf(self) -> Dict[str, Any]:
387
396
  return {
@@ -405,54 +414,45 @@ class SovereignConfig(BaseSettings):
405
414
  sources: List[ConfiguredSource]
406
415
  templates: Dict[str, Dict[str, Union[str, Loadable]]]
407
416
  template_context: Dict[str, Any] = {}
408
- eds_priority_matrix: Dict[str, Dict[str, str]] = {}
417
+ eds_priority_matrix: Dict[str, Dict[str, int]] = {}
409
418
  modifiers: List[str] = []
410
419
  global_modifiers: List[str] = []
411
420
  regions: List[str] = []
412
421
  statsd: StatsdConfig = StatsdConfig()
413
- auth_enabled: bool = False
414
- auth_passwords: str = ""
415
- encryption_key: str = ""
416
- environment: str = "local"
417
- debug_enabled: bool = False
418
- sentry_dsn: str = ""
419
- node_match_key: str = "cluster"
420
- node_matching: bool = True
421
- source_match_key: str = "service_clusters"
422
- sources_refresh_rate: int = 30
423
- cache_strategy: str = "context"
424
- refresh_context: bool = False
425
- context_refresh_rate: Optional[int]
426
- context_refresh_cron: Optional[str]
427
- dns_hard_fail: bool = False
428
- enable_application_logs: bool = True
429
- enable_access_logs: bool = True
430
- log_fmt: Optional[str] = ""
431
- ignore_empty_log_fields: bool = False
422
+ auth_enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
423
+ auth_passwords: str = Field("", alias="SOVEREIGN_AUTH_PASSWORDS")
424
+ encryption_key: str = Field("", alias="SOVEREIGN_ENCRYPTION_KEY")
425
+ environment: str = Field("local", alias="SOVEREIGN_ENVIRONMENT")
426
+ debug_enabled: bool = Field(False, alias="SOVEREIGN_DEBUG_ENABLED")
427
+ sentry_dsn: str = Field("", alias="SOVEREIGN_SENTRY_DSN")
428
+ node_match_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
429
+ node_matching: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING")
430
+ source_match_key: str = Field(
431
+ "service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY"
432
+ )
433
+ sources_refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
434
+ cache_strategy: str = Field("context", alias="SOVEREIGN_CACHE_STRATEGY")
435
+ refresh_context: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
436
+ context_refresh_rate: Optional[int] = Field(
437
+ None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE"
438
+ )
439
+ context_refresh_cron: Optional[str] = Field(
440
+ None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON"
441
+ )
442
+ dns_hard_fail: bool = Field(False, alias="SOVEREIGN_DNS_HARD_FAIL")
443
+ enable_application_logs: bool = Field(
444
+ True, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS"
445
+ )
446
+ enable_access_logs: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
447
+ log_fmt: Optional[str] = Field("", alias="SOVEREIGN_LOG_FORMAT")
448
+ ignore_empty_log_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
432
449
  discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
433
-
434
- class Config:
435
- fields = {
436
- "auth_enabled": {"env": "SOVEREIGN_AUTH_ENABLED"},
437
- "auth_passwords": {"env": "SOVEREIGN_AUTH_PASSWORDS"},
438
- "encryption_key": {"env": "SOVEREIGN_ENCRYPTION_KEY"},
439
- "environment": {"env": "SOVEREIGN_ENVIRONMENT"},
440
- "debug_enabled": {"env": "SOVEREIGN_DEBUG_ENABLED"},
441
- "sentry_dsn": {"env": "SOVEREIGN_SENTRY_DSN"},
442
- "node_match_key": {"env": "SOVEREIGN_NODE_MATCH_KEY"},
443
- "node_matching": {"env": "SOVEREIGN_NODE_MATCHING"},
444
- "source_match_key": {"env": "SOVEREIGN_SOURCE_MATCH_KEY"},
445
- "sources_refresh_rate": {"env": "SOVEREIGN_SOURCES_REFRESH_RATE"},
446
- "cache_strategy": {"env": "SOVEREIGN_CACHE_STRATEGY"},
447
- "refresh_context": {"env": "SOVEREIGN_REFRESH_CONTEXT"},
448
- "context_refresh_rate": {"env": "SOVEREIGN_CONTEXT_REFRESH_RATE"},
449
- "context_refresh_cron": {"env": "SOVEREIGN_CONTEXT_REFRESH_CRON"},
450
- "dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
451
- "enable_application_logs": {"env": "SOVEREIGN_ENABLE_APPLICATION_LOGS"},
452
- "enable_access_logs": {"env": "SOVEREIGN_ENABLE_ACCESS_LOGS"},
453
- "log_fmt": {"env": "SOVEREIGN_LOG_FORMAT"},
454
- "ignore_empty_fields": {"env": "SOVEREIGN_LOG_IGNORE_EMPTY"},
455
- }
450
+ model_config = SettingsConfigDict(
451
+ env_file=".env",
452
+ extra="ignore",
453
+ env_file_encoding="utf-8",
454
+ populate_by_name=True,
455
+ )
456
456
 
457
457
  @property
458
458
  def passwords(self) -> List[str]:
@@ -492,16 +492,15 @@ class TemplateSpecification(BaseModel):
492
492
 
493
493
 
494
494
  class NodeMatching(BaseSettings):
495
- enabled: bool = True
496
- source_key: str = "service_clusters"
497
- node_key: str = "cluster"
498
-
499
- class Config:
500
- fields = {
501
- "enabled": {"env": "SOVEREIGN_NODE_MATCHING_ENABLED"},
502
- "source_key": {"env": "SOVEREIGN_SOURCE_MATCH_KEY"},
503
- "node_key": {"env": "SOVEREIGN_NODE_MATCH_KEY"},
504
- }
495
+ enabled: bool = Field(True, alias="SOVEREIGN_NODE_MATCHING_ENABLED")
496
+ source_key: str = Field("service_clusters", alias="SOVEREIGN_SOURCE_MATCH_KEY")
497
+ node_key: str = Field("cluster", alias="SOVEREIGN_NODE_MATCH_KEY")
498
+ model_config = SettingsConfigDict(
499
+ env_file=".env",
500
+ extra="ignore",
501
+ env_file_encoding="utf-8",
502
+ populate_by_name=True,
503
+ )
505
504
 
506
505
 
507
506
  @dataclass
@@ -511,9 +510,15 @@ class EncryptionConfig:
511
510
 
512
511
 
513
512
  class AuthConfiguration(BaseSettings):
514
- enabled: bool = False
515
- auth_passwords: SecretStr = SecretStr("")
516
- encryption_key: SecretStr = SecretStr("")
513
+ enabled: bool = Field(False, alias="SOVEREIGN_AUTH_ENABLED")
514
+ auth_passwords: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_AUTH_PASSWORDS")
515
+ encryption_key: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_ENCRYPTION_KEY")
516
+ model_config = SettingsConfigDict(
517
+ env_file=".env",
518
+ extra="ignore",
519
+ env_file_encoding="utf-8",
520
+ populate_by_name=True,
521
+ )
517
522
 
518
523
  @staticmethod
519
524
  def _create_encryption_config(encryption_key_setting: str) -> EncryptionConfig:
@@ -534,37 +539,29 @@ class AuthConfiguration(BaseSettings):
534
539
  )
535
540
  return configs
536
541
 
537
- class Config:
538
- fields = {
539
- "enabled": {"env": "SOVEREIGN_AUTH_ENABLED"},
540
- "auth_passwords": {"env": "SOVEREIGN_AUTH_PASSWORDS"},
541
- "encryption_key": {"env": "SOVEREIGN_ENCRYPTION_KEY"},
542
- }
543
-
544
542
 
545
543
  class ApplicationLogConfiguration(BaseSettings):
546
- enabled: bool = False
547
- log_fmt: Optional[str] = None
544
+ enabled: bool = Field(False, alias="SOVEREIGN_ENABLE_APPLICATION_LOGS")
545
+ log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_APPLICATION_LOG_FORMAT")
548
546
  # currently only support /dev/stdout as JSON
549
-
550
- class Config:
551
- fields = {
552
- "enabled": {"env": "SOVEREIGN_ENABLE_APPLICATION_LOGS"},
553
- "log_fmt": {"env": "SOVEREIGN_APPLICATION_LOG_FORMAT"},
554
- }
547
+ model_config = SettingsConfigDict(
548
+ env_file=".env",
549
+ extra="ignore",
550
+ env_file_encoding="utf-8",
551
+ populate_by_name=True,
552
+ )
555
553
 
556
554
 
557
555
  class AccessLogConfiguration(BaseSettings):
558
- enabled: bool = True
559
- log_fmt: Optional[str] = None
560
- ignore_empty_fields: bool = False
561
-
562
- class Config:
563
- fields = {
564
- "enabled": {"env": "SOVEREIGN_ENABLE_ACCESS_LOGS"},
565
- "log_fmt": {"env": "SOVEREIGN_LOG_FORMAT"},
566
- "ignore_empty_fields": {"env": "SOVEREIGN_LOG_IGNORE_EMPTY"},
567
- }
556
+ enabled: bool = Field(True, alias="SOVEREIGN_ENABLE_ACCESS_LOGS")
557
+ log_fmt: Optional[str] = Field(None, alias="SOVEREIGN_LOG_FORMAT")
558
+ ignore_empty_fields: bool = Field(False, alias="SOVEREIGN_LOG_IGNORE_EMPTY")
559
+ model_config = SettingsConfigDict(
560
+ env_file=".env",
561
+ extra="ignore",
562
+ env_file_encoding="utf-8",
563
+ populate_by_name=True,
564
+ )
568
565
 
569
566
 
570
567
  class LoggingConfiguration(BaseSettings):
@@ -574,11 +571,19 @@ class LoggingConfiguration(BaseSettings):
574
571
 
575
572
  class ContextConfiguration(BaseSettings):
576
573
  context: Dict[str, Loadable] = {}
577
- refresh: bool = False
578
- refresh_rate: Optional[int] = None
579
- refresh_cron: Optional[str] = None
580
- refresh_num_retries: int = 3
581
- refresh_retry_interval_secs: int = 10
574
+ refresh: bool = Field(False, alias="SOVEREIGN_REFRESH_CONTEXT")
575
+ refresh_rate: Optional[int] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_RATE")
576
+ refresh_cron: Optional[str] = Field(None, alias="SOVEREIGN_CONTEXT_REFRESH_CRON")
577
+ refresh_num_retries: int = Field(3, alias="SOVEREIGN_CONTEXT_REFRESH_NUM_RETRIES")
578
+ refresh_retry_interval_secs: int = Field(
579
+ 10, alias="SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
580
+ )
581
+ model_config = SettingsConfigDict(
582
+ env_file=".env",
583
+ extra="ignore",
584
+ env_file_encoding="utf-8",
585
+ populate_by_name=True,
586
+ )
582
587
 
583
588
  @staticmethod
584
589
  def context_from_legacy(context: Dict[str, str]) -> Dict[str, Loadable]:
@@ -587,20 +592,16 @@ class ContextConfiguration(BaseSettings):
587
592
  ret[key] = Loadable.from_legacy_fmt(value)
588
593
  return ret
589
594
 
590
- @root_validator(pre=False)
591
- def validate_single_use_refresh_method(
592
- cls, values: Dict[str, Any]
593
- ) -> Dict[str, Any]:
594
- refresh_rate = values.get("refresh_rate")
595
- refresh_cron = values.get("refresh_cron")
596
-
597
- if (refresh_rate is not None) and (refresh_cron is not None):
595
+ @model_validator(mode="after")
596
+ def validate_single_use_refresh_method(self) -> Self:
597
+ if (self.refresh_rate is not None) and (self.refresh_cron is not None):
598
598
  raise RuntimeError(
599
- f"Only one of SOVEREIGN_CONTEXT_REFRESH_RATE or SOVEREIGN_CONTEXT_REFRESH_CRON can be defined. Got {refresh_rate=} and {refresh_cron=}"
599
+ f"Only one of SOVEREIGN_CONTEXT_REFRESH_RATE or SOVEREIGN_CONTEXT_REFRESH_CRON can be defined. Got {self.refresh_rate=} and {self.refresh_cron=}"
600
600
  )
601
- return values
601
+ return self
602
602
 
603
- @root_validator
603
+ @model_validator(mode="before")
604
+ @classmethod
604
605
  def set_default_refresh_rate(cls, values: Dict[str, Any]) -> Dict[str, Any]:
605
606
  refresh_rate = values.get("refresh_rate")
606
607
  refresh_cron = values.get("refresh_cron")
@@ -609,7 +610,8 @@ class ContextConfiguration(BaseSettings):
609
610
  values["refresh_rate"] = 3600
610
611
  return values
611
612
 
612
- @validator("refresh_cron")
613
+ @field_validator("refresh_cron")
614
+ @classmethod
613
615
  def validate_refresh_cron(cls, v: Optional[str]) -> Optional[str]:
614
616
  if v is None:
615
617
  return v
@@ -617,36 +619,34 @@ class ContextConfiguration(BaseSettings):
617
619
  raise CroniterBadCronError(f"'{v}' is not a valid cron expression")
618
620
  return v
619
621
 
620
- class Config:
621
- fields = {
622
- "refresh": {"env": "SOVEREIGN_REFRESH_CONTEXT"},
623
- "refresh_rate": {"env": "SOVEREIGN_CONTEXT_REFRESH_RATE"},
624
- "refresh_cron": {"env": "SOVEREIGN_CONTEXT_REFRESH_CRON"},
625
- "refresh_num_retries": {"env": "SOVEREIGN_CONTEXT_REFRESH_NUM_RETRIES"},
626
- "refresh_retry_interval_secs": {
627
- "env": "SOVEREIGN_CONTEXT_REFRESH_RETRY_INTERVAL_SECS"
628
- },
629
- }
630
-
631
622
 
632
623
  class SourcesConfiguration(BaseSettings):
633
- refresh_rate: int = 30
634
- cache_strategy: CacheStrategy = CacheStrategy.context
635
-
636
- class Config:
637
- fields = {
638
- "refresh_rate": {"env": "SOVEREIGN_SOURCES_REFRESH_RATE"},
639
- "cache_strategy": {"env": "SOVEREIGN_CACHE_STRATEGY"},
640
- }
624
+ refresh_rate: int = Field(30, alias="SOVEREIGN_SOURCES_REFRESH_RATE")
625
+ cache_strategy: CacheStrategy = Field(
626
+ CacheStrategy.context, alias="SOVEREIGN_CACHE_STRATEGY"
627
+ )
628
+ model_config = SettingsConfigDict(
629
+ env_file=".env",
630
+ extra="ignore",
631
+ env_file_encoding="utf-8",
632
+ populate_by_name=True,
633
+ )
641
634
 
642
635
 
643
636
  class LegacyConfig(BaseSettings):
644
637
  regions: Optional[List[str]] = None
645
- eds_priority_matrix: Optional[Dict[str, Dict[str, str]]] = None
646
- dns_hard_fail: Optional[bool] = None
647
- environment: Optional[str] = None
638
+ eds_priority_matrix: Optional[Dict[str, Dict[str, int]]] = None
639
+ dns_hard_fail: Optional[bool] = Field(None, alias="SOVEREIGN_DNS_HARD_FAIL")
640
+ environment: Optional[str] = Field(None, alias="SOVEREIGN_ENVIRONMENT")
641
+ model_config = SettingsConfigDict(
642
+ env_file=".env",
643
+ extra="ignore",
644
+ env_file_encoding="utf-8",
645
+ populate_by_name=True,
646
+ )
648
647
 
649
- @validator("regions")
648
+ @field_validator("regions")
649
+ @classmethod
650
650
  def regions_is_set(cls, v: Optional[List[str]]) -> List[str]:
651
651
  if v is not None:
652
652
  warnings.warn(
@@ -659,7 +659,8 @@ class LegacyConfig(BaseSettings):
659
659
  else:
660
660
  return []
661
661
 
662
- @validator("eds_priority_matrix")
662
+ @field_validator("eds_priority_matrix")
663
+ @classmethod
663
664
  def eds_priority_matrix_is_set(
664
665
  cls, v: Optional[Dict[str, Dict[str, Any]]]
665
666
  ) -> Dict[str, Dict[str, Any]]:
@@ -674,7 +675,8 @@ class LegacyConfig(BaseSettings):
674
675
  else:
675
676
  return {}
676
677
 
677
- @validator("dns_hard_fail")
678
+ @field_validator("dns_hard_fail")
679
+ @classmethod
678
680
  def dns_hard_fail_is_set(cls, v: Optional[bool]) -> bool:
679
681
  if v is not None:
680
682
  warnings.warn(
@@ -688,7 +690,8 @@ class LegacyConfig(BaseSettings):
688
690
  else:
689
691
  return False
690
692
 
691
- @validator("environment")
693
+ @field_validator("environment")
694
+ @classmethod
692
695
  def environment_is_set(cls, v: Optional[str]) -> Optional[str]:
693
696
  if v is not None:
694
697
  warnings.warn(
@@ -701,12 +704,6 @@ class LegacyConfig(BaseSettings):
701
704
  else:
702
705
  return None
703
706
 
704
- class Config:
705
- fields = {
706
- "dns_hard_fail": {"env": "SOVEREIGN_DNS_HARD_FAIL"},
707
- "environment": {"env": "SOVEREIGN_ENVIRONMENT"},
708
- }
709
-
710
707
 
711
708
  class SovereignConfigv2(BaseSettings):
712
709
  sources: List[ConfiguredSource]
@@ -719,16 +716,16 @@ class SovereignConfigv2(BaseSettings):
719
716
  authentication: AuthConfiguration = AuthConfiguration()
720
717
  logging: LoggingConfiguration = LoggingConfiguration()
721
718
  statsd: StatsdConfig = StatsdConfig()
722
- sentry_dsn: SecretStr = SecretStr("")
723
- debug: bool = False
719
+ sentry_dsn: SecretStr = Field(SecretStr(""), alias="SOVEREIGN_SENTRY_DSN")
720
+ debug: bool = Field(False, alias="SOVEREIGN_DEBUG")
724
721
  legacy_fields: LegacyConfig = LegacyConfig()
725
722
  discovery_cache: DiscoveryCacheConfig = DiscoveryCacheConfig()
726
-
727
- class Config:
728
- fields = {
729
- "sentry_dsn": {"env": "SOVEREIGN_SENTRY_DSN"},
730
- "debug": {"env": "SOVEREIGN_DEBUG"},
731
- }
723
+ model_config = SettingsConfigDict(
724
+ env_file=".env",
725
+ extra="ignore",
726
+ env_file_encoding="utf-8",
727
+ populate_by_name=True,
728
+ )
732
729
 
733
730
  @property
734
731
  def passwords(self) -> List[str]:
@@ -751,10 +748,10 @@ class SovereignConfigv2(BaseSettings):
751
748
  return self.__repr__()
752
749
 
753
750
  def __repr__(self) -> str:
754
- return f"SovereignConfigv2({self.dict()})"
751
+ return f"SovereignConfigv2({self.model_dump()})"
755
752
 
756
753
  def show(self) -> Dict[str, Any]:
757
- return self.dict()
754
+ return self.model_dump()
758
755
 
759
756
  @staticmethod
760
757
  def from_legacy_config(other: SovereignConfig) -> "SovereignConfigv2":
@@ -0,0 +1,9 @@
1
+ from typing import Any
2
+ from sovereign.config_loader import CustomLoader, Serialization
3
+
4
+
5
+ class Multiply(CustomLoader):
6
+ @staticmethod
7
+ def load(path: str, ser: Serialization) -> Any:
8
+ result = path * 2
9
+ return f"{ser}:{result}"
@@ -79,6 +79,8 @@ class CipherContainer:
79
79
 
80
80
  @property
81
81
  def key_available(self) -> bool:
82
+ if not self.suites:
83
+ return False
82
84
  return self.suites[0].key_available
83
85
 
84
86
  AVAILABLE_CIPHERS: dict[EncryptionType | Literal["default"], type[CipherSuite]] = {
@@ -16,7 +16,7 @@ router = APIRouter()
16
16
  @router.get("/xds_dump", summary="Displays all xDS resources as JSON")
17
17
  async def display_config(
18
18
  xds_type: str = Query(
19
- ..., title="xDS type", description="The type of request", example="clusters"
19
+ ..., title="xDS type", description="The type of request", examples=["clusters"]
20
20
  ),
21
21
  service_cluster: str = Query(
22
22
  "*", title="The clients service cluster to emulate in this XDS request"
@@ -48,7 +48,7 @@ async def display_config(
48
48
  )
49
49
  async def debug_template(
50
50
  xds_type: str = Query(
51
- ..., title="xDS type", description="The type of request", example="clusters"
51
+ ..., title="xDS type", description="The type of request", examples=["clusters"]
52
52
  ),
53
53
  service_cluster: str = Query(
54
54
  "*", title="The clients service cluster to emulate in this XDS request"
File without changes
File without changes