c2cwsgiutils 6.1.0.dev105__py3-none-any.whl → 6.1.7.dev4__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.
Files changed (56) hide show
  1. c2cwsgiutils/__init__.py +14 -11
  2. c2cwsgiutils/acceptance/__init__.py +2 -3
  3. c2cwsgiutils/acceptance/connection.py +1 -2
  4. c2cwsgiutils/acceptance/image.py +17 -11
  5. c2cwsgiutils/acceptance/package-lock.json +306 -213
  6. c2cwsgiutils/acceptance/package.json +2 -2
  7. c2cwsgiutils/acceptance/print.py +7 -3
  8. c2cwsgiutils/acceptance/utils.py +1 -3
  9. c2cwsgiutils/auth.py +27 -25
  10. c2cwsgiutils/broadcast/__init__.py +15 -16
  11. c2cwsgiutils/broadcast/interface.py +3 -3
  12. c2cwsgiutils/broadcast/local.py +1 -0
  13. c2cwsgiutils/broadcast/redis.py +13 -12
  14. c2cwsgiutils/client_info.py +19 -1
  15. c2cwsgiutils/coverage_setup.py +4 -3
  16. c2cwsgiutils/db.py +35 -41
  17. c2cwsgiutils/db_maintenance_view.py +13 -13
  18. c2cwsgiutils/debug/__init__.py +2 -2
  19. c2cwsgiutils/debug/_listeners.py +2 -7
  20. c2cwsgiutils/debug/_views.py +20 -12
  21. c2cwsgiutils/debug/utils.py +9 -9
  22. c2cwsgiutils/errors.py +13 -15
  23. c2cwsgiutils/health_check.py +24 -30
  24. c2cwsgiutils/index.py +34 -13
  25. c2cwsgiutils/loader.py +21 -2
  26. c2cwsgiutils/logging_view.py +12 -12
  27. c2cwsgiutils/models_graph.py +0 -1
  28. c2cwsgiutils/pretty_json.py +0 -1
  29. c2cwsgiutils/prometheus.py +1 -7
  30. c2cwsgiutils/pyramid.py +0 -1
  31. c2cwsgiutils/pyramid_logging.py +1 -1
  32. c2cwsgiutils/redis_stats.py +9 -9
  33. c2cwsgiutils/redis_utils.py +19 -18
  34. c2cwsgiutils/request_tracking/__init__.py +13 -13
  35. c2cwsgiutils/request_tracking/_sql.py +0 -1
  36. c2cwsgiutils/scripts/genversion.py +5 -5
  37. c2cwsgiutils/scripts/stats_db.py +19 -17
  38. c2cwsgiutils/scripts/test_print.py +5 -5
  39. c2cwsgiutils/sentry.py +55 -20
  40. c2cwsgiutils/services.py +2 -2
  41. c2cwsgiutils/setup_process.py +0 -1
  42. c2cwsgiutils/sql_profiler/__init__.py +5 -6
  43. c2cwsgiutils/sql_profiler/_impl.py +18 -17
  44. c2cwsgiutils/sqlalchemylogger/README.md +30 -13
  45. c2cwsgiutils/sqlalchemylogger/handlers.py +12 -11
  46. c2cwsgiutils/stats_pyramid/__init__.py +1 -5
  47. c2cwsgiutils/stats_pyramid/_db_spy.py +2 -2
  48. c2cwsgiutils/stats_pyramid/_pyramid_spy.py +12 -1
  49. c2cwsgiutils/templates/index.html.mako +4 -1
  50. c2cwsgiutils/version.py +11 -5
  51. {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dev4.dist-info}/LICENSE +1 -1
  52. {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dev4.dist-info}/METADATA +18 -6
  53. c2cwsgiutils-6.1.7.dev4.dist-info/RECORD +67 -0
  54. {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dev4.dist-info}/WHEEL +1 -1
  55. c2cwsgiutils-6.1.0.dev105.dist-info/RECORD +0 -67
  56. {c2cwsgiutils-6.1.0.dev105.dist-info → c2cwsgiutils-6.1.7.dev4.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "dependencies": {
3
- "commander": "12.0.0",
4
- "puppeteer": "22.3.0"
3
+ "commander": "12.1.0",
4
+ "puppeteer": "23.5.3"
5
5
  },
6
6
  "type": "module"
7
7
  }
@@ -7,7 +7,7 @@ import requests
7
7
 
8
8
  from c2cwsgiutils.acceptance import connection, utils
9
9
 
10
- LOG = logging.getLogger(__name__)
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
- LOG.debug("create_report=%s", create_report)
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
- LOG.debug("status=%s", repr(status))
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)
@@ -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
- COOKIE_AGE = 7 * 24 * 3600
14
+ _COOKIE_AGE = 7 * 24 * 3600
15
15
  SECRET_PROP = "c2c.secret" # nosec # noqa
16
16
  SECRET_ENV = "C2C_SECRET" # nosec # noqa
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"
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
- GITHUB_REPO_URL_PROP = "c2c.auth.github.repo_url"
28
- GITHUB_REPO_URL_ENV = "C2C_AUTH_GITHUB_REPO_URL"
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
- LOG = logging.getLogger(__name__)
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
- # logout
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
- # login or refresh the cookie
96
- request.response.set_cookie(SECRET_ENV, secret_hash, max_age=COOKIE_AGE, httponly=True)
97
- # since this could be used from outside c2cwsgiutils views, we cannot set the path to c2c
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
- LOG.warning("Error on decoding JWT token: %s", e)
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, GITHUB_REPOSITORY_ENV, GITHUB_REPOSITORY_PROP, "") != ""
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
- LOG.error(
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
- `repo` is the repository to check access to (<organization>/<repository>).
207
- `access_type` is the type of access to check (admin|push|pull).
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
- GITHUB_REPOSITORY_ENV,
224
- GITHUB_REPOSITORY_PROP,
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
- GITHUB_ACCESS_TYPE_ENV,
234
- GITHUB_ACCESS_TYPE_PROP,
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
- GITHUB_REPO_URL_ENV,
263
- GITHUB_REPO_URL_PROP,
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
- LOG = logging.getLogger(__name__)
14
- BROADCAST_ENV_KEY = "C2C_BROADCAST_PREFIX"
15
- BROADCAST_CONFIG_KEY = "c2c.broadcast_prefix"
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, BROADCAST_ENV_KEY, BROADCAST_CONFIG_KEY, "broadcast_api_"
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
- LOG.info("Broadcast service setup using Redis implementation")
40
+ _LOG.info("Broadcast service setup using Redis implementation")
41
41
  else:
42
42
  _broadcaster = local.LocalBroadcaster()
43
- LOG.info("Broadcast service setup using local implementation")
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
- LOG.info("Switching from a local broadcaster to a Redis broadcaster")
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
- LOG.error("Broadcast functionality used before it is setup")
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
- _DECORATOR_RETURN = TypeVar("_DECORATOR_RETURN")
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[..., _DECORATOR_RETURN]], Callable[..., Optional[list[_DECORATOR_RETURN]]]]:
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[..., _DECORATOR_RETURN]) -> Callable[..., Optional[list[_DECORATOR_RETURN]]]:
110
+ def impl(func: Callable[..., _DecoratorReturn]) -> Callable[..., Optional[list[_DecoratorReturn]]]:
112
111
  @functools.wraps(func)
113
- def wrapper(**kwargs: Any) -> Optional[list[_DECORATOR_RETURN]]:
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
- pass # pragma: no cover
11
+ """Subscribe to a channel."""
12
12
 
13
13
  @abstractmethod
14
14
  def unsubscribe(self, channel: str) -> None:
15
- pass # pragma: no cover
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
- pass # pragma: no cover
21
+ """Broadcast a message to a channel."""
@@ -25,4 +25,5 @@ class LocalBroadcaster(interface.BaseBroadcaster):
25
25
  return answers if expect_answers else None
26
26
 
27
27
  def get_subscribers(self) -> Mapping[str, Callable[..., Any]]:
28
+ """Get the subscribers for testing purposes."""
28
29
  return self._subscribers
@@ -11,7 +11,7 @@ import redis
11
11
 
12
12
  from c2cwsgiutils.broadcast import interface, local, utils
13
13
 
14
- LOG = logging.getLogger(__name__)
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
- LOG.debug("Received a broadcast on %s: %s", message["channel"], repr(message["data"]))
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
- LOG.error("Failed handling a broadcast message", exc_info=True)
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
- LOG.debug("Sending broadcast answer on %s", answer_channel)
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
- LOG.debug("Subscribing %s.%s to %s", callback.__module__, callback.__name__, actual_channel)
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
- LOG.debug("Unsubscribing from %s")
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
- LOG.debug("Received a broadcast answer on %s", msg["channel"])
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
- LOG.debug("Subscribing for broadcast answers on %s", answer_channel)
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
- LOG.warning(
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
- LOG.debug("Sending a broadcast on %s", actual_channel)
118
+ _LOG.debug("Sending a broadcast on %s", actual_channel)
119
119
  nb_received = self._master.publish(actual_channel, json.dumps(message))
120
- LOG.debug("Broadcast on %s sent to %d listeners", actual_channel, nb_received)
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)
@@ -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
- fields = dict(tuple(f.split("=", maxsplit=1)) for f in forwarded.split(";"))
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
@@ -5,7 +5,7 @@ from typing import Optional
5
5
 
6
6
  import pyramid.config
7
7
 
8
- LOG = logging.getLogger(__name__)
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
- LOG.warning("Setting up code coverage")
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(