c2cwsgiutils 6.1.0.dev105__py3-none-any.whl → 6.1.7__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.
- c2cwsgiutils/__init__.py +14 -11
- c2cwsgiutils/acceptance/__init__.py +2 -3
- c2cwsgiutils/acceptance/connection.py +1 -2
- c2cwsgiutils/acceptance/image.py +17 -11
- c2cwsgiutils/acceptance/package-lock.json +306 -213
- c2cwsgiutils/acceptance/package.json +2 -2
- c2cwsgiutils/acceptance/print.py +7 -3
- c2cwsgiutils/acceptance/utils.py +1 -3
- c2cwsgiutils/auth.py +27 -25
- c2cwsgiutils/broadcast/__init__.py +15 -16
- c2cwsgiutils/broadcast/interface.py +3 -3
- c2cwsgiutils/broadcast/local.py +1 -0
- c2cwsgiutils/broadcast/redis.py +13 -12
- c2cwsgiutils/client_info.py +19 -1
- c2cwsgiutils/coverage_setup.py +4 -3
- c2cwsgiutils/db.py +35 -41
- c2cwsgiutils/db_maintenance_view.py +13 -13
- c2cwsgiutils/debug/__init__.py +2 -2
- c2cwsgiutils/debug/_listeners.py +2 -7
- c2cwsgiutils/debug/_views.py +20 -12
- c2cwsgiutils/debug/utils.py +9 -9
- c2cwsgiutils/errors.py +13 -15
- c2cwsgiutils/health_check.py +24 -30
- c2cwsgiutils/index.py +34 -13
- c2cwsgiutils/loader.py +21 -2
- c2cwsgiutils/logging_view.py +12 -12
- c2cwsgiutils/models_graph.py +0 -1
- c2cwsgiutils/pretty_json.py +0 -1
- c2cwsgiutils/prometheus.py +10 -10
- c2cwsgiutils/pyramid.py +0 -1
- c2cwsgiutils/pyramid_logging.py +1 -1
- c2cwsgiutils/redis_stats.py +9 -9
- c2cwsgiutils/redis_utils.py +19 -18
- c2cwsgiutils/request_tracking/__init__.py +13 -13
- c2cwsgiutils/request_tracking/_sql.py +0 -1
- c2cwsgiutils/scripts/genversion.py +5 -5
- c2cwsgiutils/scripts/stats_db.py +19 -17
- c2cwsgiutils/scripts/test_print.py +5 -5
- c2cwsgiutils/sentry.py +55 -20
- c2cwsgiutils/services.py +2 -2
- c2cwsgiutils/setup_process.py +0 -1
- c2cwsgiutils/sql_profiler/__init__.py +5 -6
- c2cwsgiutils/sql_profiler/_impl.py +18 -17
- c2cwsgiutils/sqlalchemylogger/README.md +30 -13
- c2cwsgiutils/sqlalchemylogger/handlers.py +12 -11
- c2cwsgiutils/stats_pyramid/__init__.py +1 -5
- c2cwsgiutils/stats_pyramid/_db_spy.py +2 -2
- c2cwsgiutils/stats_pyramid/_pyramid_spy.py +12 -1
- c2cwsgiutils/templates/index.html.mako +4 -1
- c2cwsgiutils/version.py +11 -5
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dist-info}/LICENSE +1 -1
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dist-info}/METADATA +18 -6
- c2cwsgiutils-6.1.7.dist-info/RECORD +67 -0
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dist-info}/WHEEL +1 -1
- c2cwsgiutils-6.1.0.dev105.dist-info/RECORD +0 -67
- {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/acceptance/print.py
CHANGED
@@ -7,7 +7,7 @@ import requests
|
|
7
7
|
|
8
8
|
from c2cwsgiutils.acceptance import connection, utils
|
9
9
|
|
10
|
-
|
10
|
+
_LOG = logging.getLogger(__name__)
|
11
11
|
|
12
12
|
|
13
13
|
class PrintConnection(connection.Connection):
|
@@ -30,9 +30,11 @@ class PrintConnection(connection.Connection):
|
|
30
30
|
utils.retry_timeout(functools.partial(self.get_capabilities, app=app), timeout=timeout)
|
31
31
|
|
32
32
|
def get_capabilities(self, app: str) -> Any:
|
33
|
+
"""Get the capabilities for the given application."""
|
33
34
|
return self.get_json(app + "/capabilities.json", cache_expected=connection.CacheExpected.YES)
|
34
35
|
|
35
36
|
def get_example_requests(self, app: str) -> dict[str, Any]:
|
37
|
+
"""Get the example requests for the given application."""
|
36
38
|
samples = self.get_json(app + "/exampleRequest.json", cache_expected=connection.CacheExpected.YES)
|
37
39
|
out = {}
|
38
40
|
for name, value in samples.items():
|
@@ -40,12 +42,13 @@ class PrintConnection(connection.Connection):
|
|
40
42
|
return out
|
41
43
|
|
42
44
|
def get_pdf(self, app: str, request: dict[str, Any], timeout: int = 60) -> requests.Response:
|
45
|
+
"""Create a report and wait for it to be ready."""
|
43
46
|
create_report = self.post_json(app + "/report.pdf", json=request)
|
44
|
-
|
47
|
+
_LOG.debug("create_report=%s", create_report)
|
45
48
|
ref = create_report["ref"]
|
46
49
|
|
47
50
|
status = utils.retry_timeout(functools.partial(self._check_completion, ref), timeout=timeout)
|
48
|
-
|
51
|
+
_LOG.debug("status=%s", repr(status))
|
49
52
|
assert status["status"] == "finished"
|
50
53
|
|
51
54
|
report = self.get_raw("report/" + ref)
|
@@ -59,4 +62,5 @@ class PrintConnection(connection.Connection):
|
|
59
62
|
return None
|
60
63
|
|
61
64
|
def get_apps(self) -> Any:
|
65
|
+
"""Get the list of available applications."""
|
62
66
|
return self.get_json("apps.json", cache_expected=connection.CacheExpected.YES)
|
c2cwsgiutils/acceptance/utils.py
CHANGED
@@ -29,12 +29,10 @@ def retry_timeout(what: Callable[[], Any], timeout: float = _DEFAULT_TIMEOUT, in
|
|
29
29
|
Retry the function until the timeout.
|
30
30
|
|
31
31
|
Arguments:
|
32
|
-
|
33
32
|
what: the function to try
|
34
33
|
timeout: the timeout to get a success
|
35
34
|
interval: the interval between try
|
36
35
|
"""
|
37
|
-
|
38
36
|
timeout = time.perf_counter() + timeout
|
39
37
|
while True:
|
40
38
|
error = ""
|
@@ -58,7 +56,7 @@ def approx(struct: Any, **kwargs: Any) -> Any:
|
|
58
56
|
|
59
57
|
See pytest.approx
|
60
58
|
"""
|
61
|
-
import boltons.iterutils
|
59
|
+
import boltons.iterutils # pylint: disable=import-outside-toplevel
|
62
60
|
|
63
61
|
if isinstance(struct, float):
|
64
62
|
return pytest.approx(struct, **kwargs)
|
c2cwsgiutils/auth.py
CHANGED
@@ -11,21 +11,21 @@ from requests_oauthlib import OAuth2Session
|
|
11
11
|
|
12
12
|
from c2cwsgiutils.config_utils import config_bool, env_or_config, env_or_settings
|
13
13
|
|
14
|
-
|
14
|
+
_COOKIE_AGE = 7 * 24 * 3600
|
15
15
|
SECRET_PROP = "c2c.secret" # nosec # noqa
|
16
16
|
SECRET_ENV = "C2C_SECRET" # nosec # noqa
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
17
|
+
_GITHUB_REPOSITORY_PROP = "c2c.auth.github.repository"
|
18
|
+
_GITHUB_REPOSITORY_ENV = "C2C_AUTH_GITHUB_REPOSITORY"
|
19
|
+
_GITHUB_ACCESS_TYPE_PROP = "c2c.auth.github.access_type"
|
20
|
+
_GITHUB_ACCESS_TYPE_ENV = "C2C_AUTH_GITHUB_ACCESS_TYPE"
|
21
21
|
GITHUB_AUTH_URL_PROP = "c2c.auth.github.auth_url"
|
22
22
|
GITHUB_AUTH_URL_ENV = "C2C_AUTH_GITHUB_AUTH_URL"
|
23
23
|
GITHUB_TOKEN_URL_PROP = "c2c.auth.github.token_url" # nosec
|
24
24
|
GITHUB_TOKEN_URL_ENV = "C2C_AUTH_GITHUB_TOKEN_URL" # nosec
|
25
25
|
GITHUB_USER_URL_PROP = "c2c.auth.github.user_url"
|
26
26
|
GITHUB_USER_URL_ENV = "C2C_AUTH_GITHUB_USER_URL"
|
27
|
-
|
28
|
-
|
27
|
+
_GITHUB_REPO_URL_PROP = "c2c.auth.github.repo_url"
|
28
|
+
_GITHUB_REPO_URL_ENV = "C2C_AUTH_GITHUB_REPO_URL"
|
29
29
|
GITHUB_CLIENT_ID_PROP = "c2c.auth.github.client_id"
|
30
30
|
GITHUB_CLIENT_ID_ENV = "C2C_AUTH_GITHUB_CLIENT_ID"
|
31
31
|
GITHUB_CLIENT_SECRET_PROP = "c2c.auth.github.client_secret" # nosec # noqa
|
@@ -44,7 +44,7 @@ USE_SESSION_PROP = "c2c.use_session"
|
|
44
44
|
USE_SESSION_ENV = "C2C_USE_SESSION"
|
45
45
|
|
46
46
|
|
47
|
-
|
47
|
+
_LOG = logging.getLogger(__name__)
|
48
48
|
|
49
49
|
|
50
50
|
class AuthConfig(TypedDict, total=False):
|
@@ -87,14 +87,16 @@ def _is_auth_secret(request: pyramid.request.Request) -> bool:
|
|
87
87
|
|
88
88
|
if secret_hash is not None:
|
89
89
|
if secret_hash == "" or secret == "": # nosec
|
90
|
-
#
|
90
|
+
# Logout
|
91
91
|
request.response.delete_cookie(SECRET_ENV)
|
92
92
|
return False
|
93
93
|
if secret_hash != _hash_secret(expected):
|
94
94
|
return False
|
95
|
-
#
|
96
|
-
request.response.set_cookie(
|
97
|
-
|
95
|
+
# Login or refresh the cookie
|
96
|
+
request.response.set_cookie(
|
97
|
+
SECRET_ENV, secret_hash, max_age=_COOKIE_AGE, httponly=True, secure=True, samesite="Strict"
|
98
|
+
)
|
99
|
+
# Since this could be used from outside c2cwsgiutils views, we cannot set the path to c2c
|
98
100
|
return True
|
99
101
|
return False
|
100
102
|
|
@@ -125,7 +127,7 @@ def _is_auth_user_github(request: pyramid.request.Request) -> tuple[bool, UserDe
|
|
125
127
|
),
|
126
128
|
)
|
127
129
|
except jwt.exceptions.InvalidTokenError as e:
|
128
|
-
|
130
|
+
_LOG.warning("Error on decoding JWT token: %s", e)
|
129
131
|
return False, {}
|
130
132
|
|
131
133
|
|
@@ -179,11 +181,11 @@ def auth_type(settings: Optional[Mapping[str, Any]]) -> Optional[AuthenticationT
|
|
179
181
|
has_client_secret = (
|
180
182
|
env_or_settings(settings, GITHUB_CLIENT_SECRET_ENV, GITHUB_CLIENT_SECRET_PROP, "") != ""
|
181
183
|
)
|
182
|
-
has_repo = env_or_settings(settings,
|
184
|
+
has_repo = env_or_settings(settings, _GITHUB_REPOSITORY_ENV, _GITHUB_REPOSITORY_PROP, "") != ""
|
183
185
|
secret = env_or_settings(settings, GITHUB_AUTH_SECRET_ENV, GITHUB_AUTH_SECRET_PROP, "")
|
184
186
|
has_secret = len(secret) >= 16
|
185
187
|
if secret and not has_secret:
|
186
|
-
|
188
|
+
_LOG.error(
|
187
189
|
"You set a too short secret (length: %i) to protect the admin page, it should have "
|
188
190
|
"at lease a length of 16",
|
189
191
|
len(secret),
|
@@ -203,10 +205,11 @@ def check_access(
|
|
203
205
|
|
204
206
|
If the authentication type is not GitHub, this function is equivalent to is_auth.
|
205
207
|
|
206
|
-
|
207
|
-
|
208
|
+
Arguments:
|
209
|
+
request: is the request object.
|
210
|
+
repo: is the repository to check access to (<organization>/<repository>).
|
211
|
+
access_type: is the type of access to check (admin|push|pull).
|
208
212
|
"""
|
209
|
-
|
210
213
|
if not is_auth(request):
|
211
214
|
return False
|
212
215
|
|
@@ -220,8 +223,8 @@ def check_access(
|
|
220
223
|
"github_repository": (
|
221
224
|
env_or_settings(
|
222
225
|
settings,
|
223
|
-
|
224
|
-
|
226
|
+
_GITHUB_REPOSITORY_ENV,
|
227
|
+
_GITHUB_REPOSITORY_PROP,
|
225
228
|
"",
|
226
229
|
)
|
227
230
|
if repo is None
|
@@ -230,8 +233,8 @@ def check_access(
|
|
230
233
|
"github_access_type": (
|
231
234
|
env_or_settings(
|
232
235
|
settings,
|
233
|
-
|
234
|
-
|
236
|
+
_GITHUB_ACCESS_TYPE_ENV,
|
237
|
+
_GITHUB_ACCESS_TYPE_PROP,
|
235
238
|
"pull",
|
236
239
|
)
|
237
240
|
if access_type is None
|
@@ -243,7 +246,6 @@ def check_access(
|
|
243
246
|
|
244
247
|
def check_access_config(request: pyramid.request.Request, auth_config: AuthConfig) -> bool:
|
245
248
|
"""Check if the user has access to the resource."""
|
246
|
-
|
247
249
|
auth, user = is_auth_user(request)
|
248
250
|
if not auth:
|
249
251
|
return False
|
@@ -259,8 +261,8 @@ def check_access_config(request: pyramid.request.Request, auth_config: AuthConfi
|
|
259
261
|
|
260
262
|
repo_url = env_or_settings(
|
261
263
|
settings,
|
262
|
-
|
263
|
-
|
264
|
+
_GITHUB_REPO_URL_ENV,
|
265
|
+
_GITHUB_REPO_URL_PROP,
|
264
266
|
"https://api.github.com/repos",
|
265
267
|
)
|
266
268
|
repository = oauth.get(f"{repo_url}/{auth_config.get('github_repository')}").json()
|
@@ -10,9 +10,9 @@ import pyramid.config
|
|
10
10
|
from c2cwsgiutils import config_utils, redis_utils
|
11
11
|
from c2cwsgiutils.broadcast import interface, local, redis
|
12
12
|
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
_LOG = logging.getLogger(__name__)
|
14
|
+
_BROADCAST_ENV_KEY = "C2C_BROADCAST_PREFIX"
|
15
|
+
_BROADCAST_CONFIG_KEY = "c2c.broadcast_prefix"
|
16
16
|
|
17
17
|
_broadcaster: Optional[interface.BaseBroadcaster] = None
|
18
18
|
|
@@ -29,38 +29,37 @@ def includeme(config: Optional[pyramid.config.Configurator] = None) -> None:
|
|
29
29
|
|
30
30
|
Otherwise, fall back to a fake local implementation.
|
31
31
|
"""
|
32
|
-
global _broadcaster
|
32
|
+
global _broadcaster # pylint: disable=global-statement
|
33
33
|
broadcast_prefix = config_utils.env_or_config(
|
34
|
-
config,
|
34
|
+
config, _BROADCAST_ENV_KEY, _BROADCAST_CONFIG_KEY, "broadcast_api_"
|
35
35
|
)
|
36
36
|
master, slave, _ = redis_utils.get(config.get_settings() if config else None)
|
37
37
|
if _broadcaster is None:
|
38
38
|
if master is not None and slave is not None:
|
39
39
|
_broadcaster = redis.RedisBroadcaster(broadcast_prefix, master, slave)
|
40
|
-
|
40
|
+
_LOG.info("Broadcast service setup using Redis implementation")
|
41
41
|
else:
|
42
42
|
_broadcaster = local.LocalBroadcaster()
|
43
|
-
|
43
|
+
_LOG.info("Broadcast service setup using local implementation")
|
44
44
|
elif isinstance(_broadcaster, local.LocalBroadcaster) and master is not None and slave is not None:
|
45
|
-
|
45
|
+
_LOG.info("Switching from a local broadcaster to a Redis broadcaster")
|
46
46
|
prev_broadcaster = _broadcaster
|
47
47
|
_broadcaster = redis.RedisBroadcaster(broadcast_prefix, master, slave)
|
48
48
|
_broadcaster.copy_local_subscriptions(prev_broadcaster)
|
49
49
|
|
50
50
|
|
51
51
|
def _get(need_init: bool = False) -> interface.BaseBroadcaster:
|
52
|
-
global _broadcaster
|
52
|
+
global _broadcaster # pylint: disable=global-statement
|
53
53
|
if _broadcaster is None:
|
54
54
|
if need_init:
|
55
|
-
|
55
|
+
_LOG.error("Broadcast functionality used before it is setup")
|
56
56
|
_broadcaster = local.LocalBroadcaster()
|
57
57
|
return _broadcaster
|
58
58
|
|
59
59
|
|
60
60
|
def cleanup() -> None:
|
61
61
|
"""Cleanup the broadcaster to force to reinitialize it."""
|
62
|
-
|
63
|
-
global _broadcaster
|
62
|
+
global _broadcaster # pylint: disable=global-statement
|
64
63
|
_broadcaster = None
|
65
64
|
|
66
65
|
|
@@ -96,21 +95,21 @@ def broadcast(
|
|
96
95
|
|
97
96
|
# We can also templatise the argument with Python 3.10
|
98
97
|
# See: https://www.python.org/dev/peps/pep-0612/
|
99
|
-
|
98
|
+
_DecoratorReturn = TypeVar("_DecoratorReturn")
|
100
99
|
|
101
100
|
|
102
101
|
def decorator(
|
103
102
|
channel: Optional[str] = None, expect_answers: bool = False, timeout: float = 10
|
104
|
-
) -> Callable[[Callable[...,
|
103
|
+
) -> Callable[[Callable[..., _DecoratorReturn]], Callable[..., Optional[list[_DecoratorReturn]]]]:
|
105
104
|
"""
|
106
105
|
Decorate function will be called through the broadcast functionality.
|
107
106
|
|
108
107
|
If expect_answers is set to True, the returned value will be a list of all the answers.
|
109
108
|
"""
|
110
109
|
|
111
|
-
def impl(func: Callable[...,
|
110
|
+
def impl(func: Callable[..., _DecoratorReturn]) -> Callable[..., Optional[list[_DecoratorReturn]]]:
|
112
111
|
@functools.wraps(func)
|
113
|
-
def wrapper(**kwargs: Any) -> Optional[list[
|
112
|
+
def wrapper(**kwargs: Any) -> Optional[list[_DecoratorReturn]]:
|
114
113
|
return broadcast(_channel, params=kwargs, expect_answers=expect_answers, timeout=timeout)
|
115
114
|
|
116
115
|
if channel is None:
|
@@ -8,14 +8,14 @@ class BaseBroadcaster:
|
|
8
8
|
|
9
9
|
@abstractmethod
|
10
10
|
def subscribe(self, channel: str, callback: Callable[..., Any]) -> None:
|
11
|
-
|
11
|
+
"""Subscribe to a channel."""
|
12
12
|
|
13
13
|
@abstractmethod
|
14
14
|
def unsubscribe(self, channel: str) -> None:
|
15
|
-
|
15
|
+
"""Unsubscribe from a channel."""
|
16
16
|
|
17
17
|
@abstractmethod
|
18
18
|
def broadcast(
|
19
19
|
self, channel: str, params: Mapping[str, Any], expect_answers: bool, timeout: float
|
20
20
|
) -> Optional[list[Any]]:
|
21
|
-
|
21
|
+
"""Broadcast a message to a channel."""
|
c2cwsgiutils/broadcast/local.py
CHANGED
c2cwsgiutils/broadcast/redis.py
CHANGED
@@ -11,7 +11,7 @@ import redis
|
|
11
11
|
|
12
12
|
from c2cwsgiutils.broadcast import interface, local, utils
|
13
13
|
|
14
|
-
|
14
|
+
_LOG = logging.getLogger(__name__)
|
15
15
|
|
16
16
|
|
17
17
|
class RedisBroadcaster(interface.BaseBroadcaster):
|
@@ -23,7 +23,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
23
23
|
master: "redis.client.Redis[str]",
|
24
24
|
slave: "redis.client.Redis[str]",
|
25
25
|
) -> None:
|
26
|
-
from c2cwsgiutils import redis_utils
|
26
|
+
from c2cwsgiutils import redis_utils # pylint: disable=import-outside-toplevel
|
27
27
|
|
28
28
|
self._master = master
|
29
29
|
self._slave = slave
|
@@ -41,24 +41,24 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
41
41
|
|
42
42
|
def subscribe(self, channel: str, callback: Callable[..., Any]) -> None:
|
43
43
|
def wrapper(message: Mapping[str, Any]) -> None:
|
44
|
-
|
44
|
+
_LOG.debug("Received a broadcast on %s: %s", message["channel"], repr(message["data"]))
|
45
45
|
data = json.loads(message["data"])
|
46
46
|
try:
|
47
47
|
response = callback(**data["params"])
|
48
48
|
except Exception as e: # pragma: no cover # pylint: disable=broad-except
|
49
|
-
|
49
|
+
_LOG.error("Failed handling a broadcast message", exc_info=True)
|
50
50
|
response = {"status": 500, "message": str(e)}
|
51
51
|
answer_channel = data.get("answer_channel")
|
52
52
|
if answer_channel is not None:
|
53
|
-
|
53
|
+
_LOG.debug("Sending broadcast answer on %s", answer_channel)
|
54
54
|
self._master.publish(answer_channel, json.dumps(utils.add_host_info(response)))
|
55
55
|
|
56
56
|
actual_channel = self._get_channel(channel)
|
57
|
-
|
57
|
+
_LOG.debug("Subscribing %s.%s to %s", callback.__module__, callback.__name__, actual_channel)
|
58
58
|
self._pub_sub.subscribe(**{actual_channel: wrapper})
|
59
59
|
|
60
60
|
def unsubscribe(self, channel: str) -> None:
|
61
|
-
|
61
|
+
_LOG.debug("Unsubscribing from %s")
|
62
62
|
actual_channel = self._get_channel(channel)
|
63
63
|
self._pub_sub.unsubscribe(actual_channel)
|
64
64
|
|
@@ -79,7 +79,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
79
79
|
assert self._thread.is_alive()
|
80
80
|
|
81
81
|
def callback(msg: Mapping[str, Any]) -> None:
|
82
|
-
|
82
|
+
_LOG.debug("Received a broadcast answer on %s", msg["channel"])
|
83
83
|
with cond:
|
84
84
|
answers.append(json.loads(msg["data"]))
|
85
85
|
cond.notify()
|
@@ -87,7 +87,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
87
87
|
answer_channel = self._get_channel(channel) + "".join(
|
88
88
|
random.choice(string.ascii_uppercase + string.digits) for _ in range(10) # nosec
|
89
89
|
)
|
90
|
-
|
90
|
+
_LOG.debug("Subscribing for broadcast answers on %s", answer_channel)
|
91
91
|
self._pub_sub.subscribe(**{answer_channel: callback})
|
92
92
|
message = {"params": params, "answer_channel": answer_channel}
|
93
93
|
|
@@ -99,7 +99,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
99
99
|
while len(answers) < nb_received:
|
100
100
|
to_wait = timeout_time - time.perf_counter()
|
101
101
|
if to_wait <= 0.0: # pragma: no cover
|
102
|
-
|
102
|
+
_LOG.warning(
|
103
103
|
"timeout waiting for %d/%d answers on %s",
|
104
104
|
len(answers),
|
105
105
|
nb_received,
|
@@ -115,11 +115,12 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
115
115
|
|
116
116
|
def _broadcast(self, channel: str, message: Mapping[str, Any]) -> int:
|
117
117
|
actual_channel = self._get_channel(channel)
|
118
|
-
|
118
|
+
_LOG.debug("Sending a broadcast on %s", actual_channel)
|
119
119
|
nb_received = self._master.publish(actual_channel, json.dumps(message))
|
120
|
-
|
120
|
+
_LOG.debug("Broadcast on %s sent to %d listeners", actual_channel, nb_received)
|
121
121
|
return nb_received
|
122
122
|
|
123
123
|
def copy_local_subscriptions(self, prev_broadcaster: local.LocalBroadcaster) -> None:
|
124
|
+
"""Copy the subscriptions from a local broadcaster."""
|
124
125
|
for channel, callback in prev_broadcaster.get_subscribers().items():
|
125
126
|
self.subscribe(channel, callback)
|
c2cwsgiutils/client_info.py
CHANGED
@@ -1,6 +1,9 @@
|
|
1
|
+
import logging
|
2
|
+
import os
|
1
3
|
import re
|
2
4
|
from typing import Any, Callable
|
3
5
|
|
6
|
+
_LOG = logging.getLogger(__name__)
|
4
7
|
SEP_RE = re.compile(r", *")
|
5
8
|
|
6
9
|
|
@@ -22,6 +25,15 @@ class Filter:
|
|
22
25
|
else:
|
23
26
|
_handle_others(environ)
|
24
27
|
|
28
|
+
if "C2CWSGIUTILS_FORCE_PROTO" in os.environ:
|
29
|
+
environ["wsgi.url_scheme"] = os.environ["C2CWSGIUTILS_FORCE_PROTO"]
|
30
|
+
if "C2CWSGIUTILS_FORCE_HOST" in os.environ:
|
31
|
+
environ["HTTP_HOST"] = os.environ["C2CWSGIUTILS_FORCE_HOST"]
|
32
|
+
if "C2CWSGIUTILS_FORCE_SERVER_NAME" in os.environ:
|
33
|
+
environ["SERVER_NAME"] = os.environ["C2CWSGIUTILS_FORCE_SERVER_NAME"]
|
34
|
+
if "C2CWSGIUTILS_FORCE_REMOTE_ADDR" in os.environ:
|
35
|
+
environ["REMOTE_ADDR"] = os.environ["C2CWSGIUTILS_FORCE_REMOTE_ADDR"]
|
36
|
+
|
25
37
|
return self._application(environ, start_response)
|
26
38
|
|
27
39
|
|
@@ -57,7 +69,12 @@ def _handle_forwarded(environ: dict[str, str]) -> None:
|
|
57
69
|
if "HTTP_" + header in environ:
|
58
70
|
environ["HTTP_ORIGINAL_" + header] = environ.pop("HTTP_" + header)
|
59
71
|
forwarded = SEP_RE.split(environ.pop("HTTP_FORWARDED"))[0]
|
60
|
-
|
72
|
+
parts = forwarded.split(";")
|
73
|
+
ignored_parts = [part for part in parts if "=" not in part]
|
74
|
+
if ignored_parts:
|
75
|
+
_LOG.warning("Some parts of the Forwarded header are ignored: %s", ";".join(ignored_parts))
|
76
|
+
parts = [part for part in parts if "=" in part]
|
77
|
+
fields = dict(tuple(f.split("=", maxsplit=1)) for f in parts)
|
61
78
|
if "by" in fields:
|
62
79
|
environ["SERVER_NAME"] = fields["by"]
|
63
80
|
if "for" in fields:
|
@@ -71,4 +88,5 @@ def _handle_forwarded(environ: dict[str, str]) -> None:
|
|
71
88
|
|
72
89
|
def filter_factory(*args: Any, **kwargs: Any) -> Callable[..., Any]:
|
73
90
|
"""Get the filter."""
|
91
|
+
del args, kwargs # unused
|
74
92
|
return Filter
|
c2cwsgiutils/coverage_setup.py
CHANGED
@@ -5,7 +5,7 @@ from typing import Optional
|
|
5
5
|
|
6
6
|
import pyramid.config
|
7
7
|
|
8
|
-
|
8
|
+
_LOG = logging.getLogger(__name__)
|
9
9
|
|
10
10
|
|
11
11
|
def init() -> None:
|
@@ -16,11 +16,12 @@ def init() -> None:
|
|
16
16
|
|
17
17
|
def includeme(config: Optional[pyramid.config.Configurator] = None) -> None:
|
18
18
|
"""Initialize the code coverage."""
|
19
|
+
del config # unused
|
19
20
|
if os.environ.get("COVERAGE", "0") != "1":
|
20
21
|
return
|
21
|
-
import coverage
|
22
|
+
import coverage # pylint: disable=import-outside-toplevel
|
22
23
|
|
23
|
-
|
24
|
+
_LOG.warning("Setting up code coverage")
|
24
25
|
report_dir = "/tmp/coverage/api" # nosec
|
25
26
|
os.makedirs(report_dir, exist_ok=True)
|
26
27
|
cov = coverage.Coverage(
|