sovereign 0.29.3__py3-none-any.whl → 0.30.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of sovereign might be problematic. Click here for more details.

sovereign/__init__.py CHANGED
@@ -1,46 +1,26 @@
1
1
  import os
2
+ import sys
2
3
  from contextvars import ContextVar
3
4
  from importlib.metadata import version
4
- from typing import Any, Mapping, Type
5
+ from typing import Optional
5
6
 
6
- from fastapi.responses import JSONResponse
7
7
  from pydantic import ValidationError
8
8
  from starlette.templating import Jinja2Templates
9
9
 
10
- from sovereign import dynamic_config
11
10
  from sovereign.context import TemplateContext
12
11
  from sovereign.logging.bootstrapper import LoggerBootstrapper
13
- from sovereign.schemas import SovereignAsgiConfig, SovereignConfig, SovereignConfigv2
12
+ from sovereign.schemas import (
13
+ SovereignAsgiConfig,
14
+ SovereignConfig,
15
+ SovereignConfigv2,
16
+ migrate_configs,
17
+ parse_raw_configuration,
18
+ )
14
19
  from sovereign.sources import SourcePoller
15
20
  from sovereign.statistics import configure_statsd
16
21
  from sovereign.utils.crypto.crypto import CipherContainer
17
- from sovereign.utils.dictupdate import merge # type: ignore
18
22
  from sovereign.utils.resources import get_package_file
19
23
 
20
- json_response_class: Type[JSONResponse] = JSONResponse
21
- try:
22
- import orjson
23
- from fastapi.responses import ORJSONResponse
24
-
25
- json_response_class = ORJSONResponse
26
- except ImportError:
27
- try:
28
- import ujson
29
- from fastapi.responses import UJSONResponse
30
-
31
- json_response_class = UJSONResponse
32
- except ImportError:
33
- pass
34
-
35
-
36
- def parse_raw_configuration(path: str) -> Mapping[Any, Any]:
37
- ret: Mapping[Any, Any] = dict()
38
- for p in path.split(","):
39
- spec = dynamic_config.Loadable.from_legacy_fmt(p)
40
- ret = merge(obj_a=ret, obj_b=spec.load(), merge_lists=True)
41
- return ret
42
-
43
-
44
24
  _request_id_ctx_var: ContextVar[str] = ContextVar("request_id", default="")
45
25
 
46
26
 
@@ -57,6 +37,9 @@ html_templates = Jinja2Templates(
57
37
  directory=str(get_package_file(DIST_NAME, "templates"))
58
38
  )
59
39
 
40
+ if sys.argv[0].endswith("sovereign"):
41
+ migrate_configs()
42
+
60
43
  try:
61
44
  config = SovereignConfigv2(**parse_raw_configuration(config_path))
62
45
  except ValidationError:
@@ -69,15 +52,25 @@ logs = LoggerBootstrapper(config)
69
52
  application_logger = logs.application_logger.logger
70
53
 
71
54
  stats = configure_statsd(config=config.statsd)
72
- poller = SourcePoller(
73
- sources=config.sources,
74
- matching_enabled=config.matching.enabled,
75
- node_match_key=config.matching.node_key,
76
- source_match_key=config.matching.source_key,
77
- source_refresh_rate=config.source_config.refresh_rate,
78
- logger=application_logger,
79
- stats=stats,
80
- )
55
+ poller = None
56
+ if config.sources is not None:
57
+ if config.matching is not None:
58
+ matching_enabled = config.matching.enabled
59
+ node_key: Optional[str] = config.matching.node_key
60
+ source_key: Optional[str] = config.matching.source_key
61
+ else:
62
+ matching_enabled = False
63
+ node_key = None
64
+ source_key = None
65
+ poller = SourcePoller(
66
+ sources=config.sources,
67
+ matching_enabled=matching_enabled,
68
+ node_match_key=node_key,
69
+ source_match_key=source_key,
70
+ source_refresh_rate=config.source_config.refresh_rate,
71
+ logger=application_logger,
72
+ stats=stats,
73
+ )
81
74
 
82
75
  encryption_configs = config.authentication.encryption_configs
83
76
  server_cipher_container = CipherContainer.from_encryption_configs(
@@ -95,5 +88,6 @@ template_context = TemplateContext(
95
88
  logger=application_logger,
96
89
  stats=stats,
97
90
  )
98
- poller.lazy_load_modifiers(config.modifiers)
99
- poller.lazy_load_global_modifiers(config.global_modifiers)
91
+ if poller is not None:
92
+ poller.lazy_load_modifiers(config.modifiers)
93
+ poller.lazy_load_global_modifiers(config.global_modifiers)
sovereign/app.py CHANGED
@@ -4,24 +4,22 @@ from collections import namedtuple
4
4
 
5
5
  import uvicorn
6
6
  from fastapi import FastAPI, Request
7
- from fastapi.responses import RedirectResponse, FileResponse, Response, JSONResponse
7
+ from fastapi.responses import FileResponse, JSONResponse, RedirectResponse, Response
8
+ from starlette_context.middleware import RawContextMiddleware
8
9
 
9
10
  from sovereign import (
10
11
  __version__,
11
- config,
12
12
  asgi_config,
13
- json_response_class,
13
+ config,
14
+ logs,
14
15
  poller,
15
16
  template_context,
16
- logs,
17
17
  )
18
+ from sovereign.response_class import json_response_class
18
19
  from sovereign.error_info import ErrorInfo
20
+ from sovereign.middlewares import LoggingMiddleware, RequestContextLogMiddleware
19
21
  from sovereign.utils.resources import get_package_file
20
- from sovereign.views import crypto, discovery, healthchecks, admin, interface
21
- from sovereign.middlewares import (
22
- RequestContextLogMiddleware,
23
- LoggingMiddleware,
24
- )
22
+ from sovereign.views import admin, crypto, discovery, healthchecks, interface
25
23
 
26
24
  Router = namedtuple("Router", "module tags prefix")
27
25
 
@@ -89,6 +87,8 @@ def init_app() -> FastAPI:
89
87
  application.add_middleware(SentryAsgiMiddleware)
90
88
  logs.application_logger.logger.info("Sentry middleware enabled")
91
89
 
90
+ application.add_middleware(RawContextMiddleware)
91
+
92
92
  @application.exception_handler(500)
93
93
  async def exception_handler(_: Request, exc: Exception) -> JSONResponse:
94
94
  """
@@ -109,7 +109,8 @@ def init_app() -> FastAPI:
109
109
 
110
110
  @application.on_event("startup")
111
111
  async def keep_sources_uptodate() -> None:
112
- asyncio.create_task(poller.poll_forever())
112
+ if poller is not None:
113
+ asyncio.create_task(poller.poll_forever())
113
114
 
114
115
  @application.on_event("startup")
115
116
  async def refresh_template_context() -> None:
sovereign/context.py CHANGED
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import warnings
2
3
  import traceback
3
4
  from copy import deepcopy
4
5
  from typing import (
@@ -37,7 +38,7 @@ class TemplateContext:
37
38
  refresh_num_retries: int,
38
39
  refresh_retry_interval_secs: int,
39
40
  configured_context: Dict[str, Loadable],
40
- poller: SourcePoller,
41
+ poller: Optional[SourcePoller],
41
42
  encryption_suite: Optional[CipherContainer],
42
43
  logger: BoundLogger,
43
44
  stats: Any,
@@ -121,41 +122,45 @@ class TemplateContext:
121
122
  if "crypto" not in self.context and self.crypto:
122
123
  self.context["crypto"] = self.crypto
123
124
 
124
- def build_new_context_from_instances(self, node_value: str) -> Dict[str, Any]:
125
- matches = self.poller.match_node(node_value=node_value)
125
+ def build_new_context_from_instances(
126
+ self, node_value: Optional[str]
127
+ ) -> Dict[str, Any]:
128
+ to_add = dict()
129
+ if self.poller is not None:
130
+ matches = self.poller.match_node(node_value=node_value)
131
+ for scope, instances in matches.scopes.items():
132
+ if scope in ("default", None):
133
+ to_add["instances"] = instances
134
+ else:
135
+ to_add[scope] = instances
136
+ if to_add == {}:
137
+ raise HTTPException(
138
+ detail=(
139
+ "This node does not match any instances! ",
140
+ "If node matching is enabled, check that the node "
141
+ "match key aligns with the source match key. "
142
+ "If you don't know what any of this is, disable "
143
+ "node matching via the config",
144
+ ),
145
+ status_code=400,
146
+ )
126
147
  ret = dict()
127
148
  for key, value in self.context.items():
128
149
  try:
129
150
  ret[key] = deepcopy(value)
130
151
  except TypeError:
131
152
  ret[key] = value
132
-
133
- to_add = dict()
134
- for scope, instances in matches.scopes.items():
135
- if scope in ("default", None):
136
- to_add["instances"] = instances
137
- else:
138
- to_add[scope] = instances
139
- if to_add == {}:
140
- raise HTTPException(
141
- detail=(
142
- "This node does not match any instances! ",
143
- "If node matching is enabled, check that the node "
144
- "match key aligns with the source match key. "
145
- "If you don't know what any of this is, disable "
146
- "node matching via the config",
147
- ),
148
- status_code=400,
149
- )
150
153
  ret.update(to_add)
151
154
  return ret
152
155
 
153
156
  def get_context(
154
157
  self, request: DiscoveryRequest, template: XdsTemplate
155
158
  ) -> Dict[str, Any]:
156
- ret = self.build_new_context_from_instances(
157
- node_value=self.poller.extract_node_key(request.node),
158
- )
159
+ ret = {}
160
+ node_value = None
161
+ if self.poller is not None and self.poller.node_match_key is not None:
162
+ node_value = self.poller.extract_node_key(request.node)
163
+ ret = self.build_new_context_from_instances(node_value=node_value)
159
164
  ret["__hide_from_ui"] = lambda v: v
160
165
  if request.hide_private_keys:
161
166
  ret["__hide_from_ui"] = lambda _: "(value hidden)"
@@ -178,4 +183,12 @@ class TemplateContext:
178
183
  yield key
179
184
 
180
185
  def get(self, *args: Any, **kwargs: Any) -> Any:
186
+ warnings.warn(
187
+ (
188
+ "Accessing values from template_context directly is deprecated and "
189
+ "will be removed in a future release."
190
+ ),
191
+ DeprecationWarning,
192
+ stacklevel=2,
193
+ )
181
194
  return self.context.get(*args, **kwargs)
sovereign/discovery.py CHANGED
@@ -6,12 +6,14 @@ Functions used to render and return discovery responses to Envoy proxies.
6
6
 
7
7
  The templates are configurable. `todo See ref:Configuration#Templates`
8
8
  """
9
+
10
+ import warnings
9
11
  from enum import Enum
10
- from typing import List, Dict, Any, Optional
12
+ from typing import Any, Dict, List, Optional
11
13
 
12
14
  import yaml
13
- from yaml.parser import ParserError, ScannerError # type: ignore
14
15
  from starlette.exceptions import HTTPException
16
+ from yaml.parser import ParserError, ScannerError # type: ignore
15
17
 
16
18
  try:
17
19
  import sentry_sdk
@@ -20,21 +22,18 @@ try:
20
22
  except ImportError:
21
23
  SENTRY_INSTALLED = False
22
24
 
23
- from sovereign import XDS_TEMPLATES, config, logs, template_context
25
+ from sovereign import XDS_TEMPLATES, config, template_context, logs
26
+ from sovereign.schemas import DiscoveryRequest, ProcessedTemplate, XdsTemplate
24
27
  from sovereign.utils.version_info import compute_hash
25
- from sovereign.schemas import XdsTemplate, DiscoveryRequest, ProcessedTemplate
26
-
27
28
 
28
29
  try:
29
30
  default_templates = XDS_TEMPLATES["default"]
30
31
  except KeyError:
31
- raise KeyError(
32
+ warnings.warn(
32
33
  "Your configuration should contain default templates. For more details, see "
33
- "https://vsyrakis.bitbucket.io/sovereign/docs/html/guides/tutorial.html#create-templates "
34
+ "https://developer.atlassian.com/platform/sovereign/tutorial/templates/#versioning-templates"
34
35
  )
35
36
 
36
- cache_strategy = config.source_config.cache_strategy
37
-
38
37
  # Create an enum that bases all the available discovery types off what has been configured
39
38
  discovery_types = (_type for _type in sorted(XDS_TEMPLATES["__any__"].keys()))
40
39
  discovery_types_base: Dict[str, str] = {t: t for t in discovery_types}
@@ -34,8 +34,7 @@ class ConfigDeserializer(Protocol):
34
34
  path: ...
35
35
  """
36
36
 
37
- def deserialize(self, input: Any) -> Any:
38
- ...
37
+ def deserialize(self, input: Any) -> Any: ...
39
38
 
40
39
 
41
40
  class YamlDeserializer(ConfigDeserializer):
@@ -31,8 +31,7 @@ class CustomLoader(Protocol):
31
31
 
32
32
  default_deser: str = "yaml"
33
33
 
34
- def load(self, path: str) -> Any:
35
- ...
34
+ def load(self, path: str) -> Any: ...
36
35
 
37
36
 
38
37
  class File(CustomLoader):
@@ -1,7 +1,9 @@
1
+ from copy import deepcopy
1
2
  from functools import cached_property
2
3
  from typing import Any, Dict
3
4
 
4
5
  import structlog
6
+ from starlette_context import context
5
7
  from structlog.stdlib import BoundLogger
6
8
 
7
9
  from sovereign.logging.base_logger import BaseLogger
@@ -20,7 +22,7 @@ class AccessLogger(BaseLogger):
20
22
  wrapper_class=structlog.BoundLogger,
21
23
  processors=[
22
24
  self.is_enabled_processor,
23
- self.merge_in_threadlocal,
25
+ self.merge_starlette_contextvars,
24
26
  self.format_access_log_fields,
25
27
  ],
26
28
  type=LoggingType.ACCESS,
@@ -70,3 +72,14 @@ class AccessLogger(BaseLogger):
70
72
  continue
71
73
  formatted_dict[k] = value
72
74
  return formatted_dict
75
+
76
+ def merge_starlette_contextvars(
77
+ self, _, __, event_dict: EventDict
78
+ ) -> ProcessedMessage:
79
+ merged_context = deepcopy(event_dict)
80
+ for k, v in context.data.items():
81
+ merged_context[k] = v
82
+ return merged_context
83
+
84
+ def queue_log_fields(self, **kwargs: Any) -> None:
85
+ context.update(kwargs)
@@ -19,7 +19,6 @@ class ApplicationLogger(BaseLogger):
19
19
  wrapper_class=structlog.BoundLogger,
20
20
  processors=[
21
21
  self.is_enabled_processor,
22
- self.merge_in_threadlocal,
23
22
  self.format_application_log_fields,
24
23
  ],
25
24
  type=LoggingType.APPLICATION,
@@ -1,29 +1,24 @@
1
1
  import json
2
- import threading
3
2
  from abc import ABC, abstractmethod
4
3
  from functools import cached_property
5
- from typing import Any, Dict, Optional
4
+ from typing import Dict, Optional
6
5
 
7
6
  from structlog.exceptions import DropEvent
8
7
  from structlog.stdlib import BoundLogger
9
8
 
10
9
  from sovereign.logging.types import EventDict, ProcessedMessage
11
10
 
12
- LOG_QUEUE = threading.local()
13
-
14
11
 
15
12
  class BaseLogger(ABC):
16
13
  _user_log_fmt: Optional[str]
17
14
 
18
15
  @property
19
16
  @abstractmethod
20
- def is_enabled(self) -> bool:
21
- ...
17
+ def is_enabled(self) -> bool: ...
22
18
 
23
19
  @property
24
20
  @abstractmethod
25
- def _default_log_fmt(self) -> Dict[str, str]:
26
- ...
21
+ def _default_log_fmt(self) -> Dict[str, str]: ...
27
22
 
28
23
  def is_enabled_processor(
29
24
  self, logger: BoundLogger, method_name: str, event_dict: EventDict
@@ -44,22 +39,3 @@ class BaseLogger(ABC):
44
39
  format["event"] = "{event}"
45
40
  return format
46
41
  return self._default_log_fmt
47
-
48
- def merge_in_threadlocal(
49
- self, logger: Any, method_name: str, event_dict: EventDict
50
- ) -> ProcessedMessage:
51
- self._ensure_threadlocal()
52
- fields: Dict[str, Any] = LOG_QUEUE.fields.copy()
53
- fields.update(event_dict)
54
- return fields
55
-
56
- def clear_log_fields(self) -> None:
57
- LOG_QUEUE.fields = dict()
58
-
59
- def _ensure_threadlocal(self) -> None:
60
- if not hasattr(LOG_QUEUE, "fields"):
61
- LOG_QUEUE.fields = dict()
62
-
63
- def queue_log_fields(self, **kwargs: Any) -> None:
64
- self._ensure_threadlocal()
65
- LOG_QUEUE.fields.update(kwargs)
@@ -29,6 +29,6 @@ class LoggerBootstrapper:
29
29
  def debug_logs_processor(
30
30
  self, logger: BoundLogger, method_name: str, event_dict: EventDict
31
31
  ) -> ProcessedMessage:
32
- if method_name == "debug" and self.show_debug == False:
32
+ if method_name == "debug" and self.show_debug is False:
33
33
  raise DropEvent
34
34
  return event_dict
sovereign/middlewares.py CHANGED
@@ -1,10 +1,12 @@
1
1
  import os
2
2
  import time
3
3
  from uuid import uuid4
4
+
4
5
  from fastapi.requests import Request
5
6
  from fastapi.responses import Response
6
7
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
7
- from sovereign import config, logs, get_request_id, _request_id_ctx_var, stats
8
+
9
+ from sovereign import _request_id_ctx_var, config, get_request_id, logs, stats
8
10
 
9
11
 
10
12
  class RequestContextLogMiddleware(BaseHTTPMiddleware):
@@ -36,7 +38,6 @@ class LoggingMiddleware(BaseHTTPMiddleware):
36
38
  source_port = addr.port
37
39
  if xff := request.headers.get("X-Forwarded-For"):
38
40
  source_ip = xff.split(",")[0] # leftmost address
39
- logs.access_logger.clear_log_fields()
40
41
  logs.access_logger.queue_log_fields(
41
42
  ENVIRONMENT=config.legacy_fields.environment,
42
43
  HOST=request.headers.get("host", "-"),
@@ -6,6 +6,7 @@ used via configuration.
6
6
 
7
7
  `todo entry point install guide`
8
8
  """
9
+
9
10
  import abc
10
11
  from typing import List, Any, Dict
11
12