c2cwsgiutils 6.1.7.dev4__py3-none-any.whl → 6.2.0.dev54__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 (48) hide show
  1. c2cwsgiutils/acceptance/__init__.py +1 -0
  2. c2cwsgiutils/acceptance/connection.py +3 -2
  3. c2cwsgiutils/acceptance/image.py +5 -3
  4. c2cwsgiutils/acceptance/package-lock.json +110 -248
  5. c2cwsgiutils/acceptance/package.json +2 -2
  6. c2cwsgiutils/acceptance/print.py +1 -0
  7. c2cwsgiutils/acceptance/utils.py +2 -1
  8. c2cwsgiutils/auth.py +9 -8
  9. c2cwsgiutils/broadcast/__init__.py +1 -1
  10. c2cwsgiutils/broadcast/local.py +4 -0
  11. c2cwsgiutils/broadcast/redis.py +8 -2
  12. c2cwsgiutils/client_info.py +2 -0
  13. c2cwsgiutils/coverage_setup.py +2 -2
  14. c2cwsgiutils/db.py +11 -2
  15. c2cwsgiutils/db_maintenance_view.py +1 -1
  16. c2cwsgiutils/debug/__init__.py +4 -2
  17. c2cwsgiutils/debug/_views.py +22 -4
  18. c2cwsgiutils/errors.py +7 -2
  19. c2cwsgiutils/health_check.py +39 -29
  20. c2cwsgiutils/index.py +1 -1
  21. c2cwsgiutils/loader.py +1 -1
  22. c2cwsgiutils/logging_view.py +1 -1
  23. c2cwsgiutils/models_graph.py +1 -1
  24. c2cwsgiutils/pretty_json.py +1 -1
  25. c2cwsgiutils/profiler.py +1 -0
  26. c2cwsgiutils/prometheus.py +58 -0
  27. c2cwsgiutils/pyramid.py +1 -0
  28. c2cwsgiutils/pyramid_logging.py +4 -0
  29. c2cwsgiutils/redis_stats.py +1 -1
  30. c2cwsgiutils/redis_utils.py +2 -0
  31. c2cwsgiutils/request_tracking/__init__.py +1 -1
  32. c2cwsgiutils/scripts/genversion.py +4 -2
  33. c2cwsgiutils/scripts/stats_db.py +1 -0
  34. c2cwsgiutils/scripts/test_print.py +4 -1
  35. c2cwsgiutils/sentry.py +1 -1
  36. c2cwsgiutils/setup_process.py +5 -1
  37. c2cwsgiutils/sql_profiler/__init__.py +1 -1
  38. c2cwsgiutils/sql_profiler/_impl.py +1 -1
  39. c2cwsgiutils/sqlalchemylogger/handlers.py +18 -12
  40. c2cwsgiutils/stats_pyramid/__init__.py +2 -1
  41. c2cwsgiutils/stats_pyramid/_pyramid_spy.py +1 -0
  42. c2cwsgiutils/version.py +1 -1
  43. {c2cwsgiutils-6.1.7.dev4.dist-info → c2cwsgiutils-6.2.0.dev54.dist-info}/METADATA +74 -12
  44. c2cwsgiutils-6.2.0.dev54.dist-info/RECORD +67 -0
  45. c2cwsgiutils-6.1.7.dev4.dist-info/RECORD +0 -67
  46. {c2cwsgiutils-6.1.7.dev4.dist-info → c2cwsgiutils-6.2.0.dev54.dist-info}/LICENSE +0 -0
  47. {c2cwsgiutils-6.1.7.dev4.dist-info → c2cwsgiutils-6.2.0.dev54.dist-info}/WHEEL +0 -0
  48. {c2cwsgiutils-6.1.7.dev4.dist-info → c2cwsgiutils-6.2.0.dev54.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/auth.py CHANGED
@@ -12,32 +12,32 @@ from requests_oauthlib import OAuth2Session
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
- SECRET_PROP = "c2c.secret" # nosec # noqa
16
- SECRET_ENV = "C2C_SECRET" # nosec # noqa
15
+ SECRET_PROP = "c2c.secret" # noqa: S105
16
+ SECRET_ENV = "C2C_SECRET" # noqa: S105
17
17
  _GITHUB_REPOSITORY_PROP = "c2c.auth.github.repository"
18
18
  _GITHUB_REPOSITORY_ENV = "C2C_AUTH_GITHUB_REPOSITORY"
19
19
  _GITHUB_ACCESS_TYPE_PROP = "c2c.auth.github.access_type"
20
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
- GITHUB_TOKEN_URL_PROP = "c2c.auth.github.token_url" # nosec
24
- GITHUB_TOKEN_URL_ENV = "C2C_AUTH_GITHUB_TOKEN_URL" # nosec
23
+ GITHUB_TOKEN_URL_PROP = "c2c.auth.github.token_url" # noqa: S105
24
+ GITHUB_TOKEN_URL_ENV = "C2C_AUTH_GITHUB_TOKEN_URL" # noqa: S105
25
25
  GITHUB_USER_URL_PROP = "c2c.auth.github.user_url"
26
26
  GITHUB_USER_URL_ENV = "C2C_AUTH_GITHUB_USER_URL"
27
27
  _GITHUB_REPO_URL_PROP = "c2c.auth.github.repo_url"
28
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
- GITHUB_CLIENT_SECRET_PROP = "c2c.auth.github.client_secret" # nosec # noqa
32
- GITHUB_CLIENT_SECRET_ENV = "C2C_AUTH_GITHUB_CLIENT_SECRET" # nosec # noqa
31
+ GITHUB_CLIENT_SECRET_PROP = "c2c.auth.github.client_secret" # noqa: S105
32
+ GITHUB_CLIENT_SECRET_ENV = "C2C_AUTH_GITHUB_CLIENT_SECRET" # noqa: S105
33
33
  GITHUB_SCOPE_PROP = "c2c.auth.github.scope"
34
34
  GITHUB_SCOPE_ENV = "C2C_AUTH_GITHUB_SCOPE"
35
35
  # To be able to use private repository
36
36
  GITHUB_SCOPE_DEFAULT = "repo"
37
37
  GITHUB_AUTH_COOKIE_PROP = "c2c.auth.github.auth.cookie"
38
38
  GITHUB_AUTH_COOKIE_ENV = "C2C_AUTH_GITHUB_COOKIE"
39
- GITHUB_AUTH_SECRET_PROP = "c2c.auth.github.auth.secret" # nosec # noqa
40
- GITHUB_AUTH_SECRET_ENV = "C2C_AUTH_GITHUB_SECRET" # nosec # noqa
39
+ GITHUB_AUTH_SECRET_PROP = "c2c.auth.github.auth.secret" # noqa: S105
40
+ GITHUB_AUTH_SECRET_ENV = "C2C_AUTH_GITHUB_SECRET" # noqa: S105
41
41
  GITHUB_AUTH_PROXY_URL_PROP = "c2c.auth.github.auth.proxy_url"
42
42
  GITHUB_AUTH_PROXY_URL_ENV = "C2C_AUTH_GITHUB_PROXY_URL"
43
43
  USE_SESSION_PROP = "c2c.use_session"
@@ -209,6 +209,7 @@ def check_access(
209
209
  request: is the request object.
210
210
  repo: is the repository to check access to (<organization>/<repository>).
211
211
  access_type: is the type of access to check (admin|push|pull).
212
+
212
213
  """
213
214
  if not is_auth(request):
214
215
  return False
@@ -19,7 +19,7 @@ _broadcaster: Optional[interface.BaseBroadcaster] = None
19
19
 
20
20
  def init(config: Optional[pyramid.config.Configurator] = None) -> None:
21
21
  """Initialize the broadcaster with Redis, if configured, for backward compatibility."""
22
- warnings.warn("init function is deprecated; use includeme instead")
22
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
23
23
  includeme(config)
24
24
 
25
25
 
@@ -9,17 +9,21 @@ class LocalBroadcaster(interface.BaseBroadcaster):
9
9
  """Fake implementation of broadcasting messages (will just answer locally)."""
10
10
 
11
11
  def __init__(self) -> None:
12
+ """Initialize the broadcaster."""
12
13
  self._subscribers: MutableMapping[str, Callable[..., Any]] = {}
13
14
 
14
15
  def subscribe(self, channel: str, callback: Callable[..., Any]) -> None:
16
+ """Subscribe to a channel."""
15
17
  self._subscribers[channel] = callback
16
18
 
17
19
  def unsubscribe(self, channel: str) -> None:
20
+ """Unsubscribe from a channel."""
18
21
  del self._subscribers[channel]
19
22
 
20
23
  def broadcast(
21
24
  self, channel: str, params: Mapping[str, Any], expect_answers: bool, timeout: float
22
25
  ) -> Optional[list[Any]]:
26
+ """Broadcast a message to all the listeners."""
23
27
  subscriber = self._subscribers.get(channel, None)
24
28
  answers = [utils.add_host_info(subscriber(**params))] if subscriber is not None else []
25
29
  return answers if expect_answers else None
@@ -23,6 +23,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
23
23
  master: "redis.client.Redis[str]",
24
24
  slave: "redis.client.Redis[str]",
25
25
  ) -> None:
26
+ """Initialize the broadcaster."""
26
27
  from c2cwsgiutils import redis_utils # pylint: disable=import-outside-toplevel
27
28
 
28
29
  self._master = master
@@ -40,6 +41,8 @@ class RedisBroadcaster(interface.BaseBroadcaster):
40
41
  return self._broadcast_prefix + channel
41
42
 
42
43
  def subscribe(self, channel: str, callback: Callable[..., Any]) -> None:
44
+ """Subscribe to a channel."""
45
+
43
46
  def wrapper(message: Mapping[str, Any]) -> None:
44
47
  _LOG.debug("Received a broadcast on %s: %s", message["channel"], repr(message["data"]))
45
48
  data = json.loads(message["data"])
@@ -58,6 +61,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
58
61
  self._pub_sub.subscribe(**{actual_channel: wrapper})
59
62
 
60
63
  def unsubscribe(self, channel: str) -> None:
64
+ """Unsubscribe from a channel."""
61
65
  _LOG.debug("Unsubscribing from %s")
62
66
  actual_channel = self._get_channel(channel)
63
67
  self._pub_sub.unsubscribe(actual_channel)
@@ -65,6 +69,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
65
69
  def broadcast(
66
70
  self, channel: str, params: Mapping[str, Any], expect_answers: bool, timeout: float
67
71
  ) -> Optional[list[Any]]:
72
+ """Broadcast a message to all the listeners."""
68
73
  if expect_answers:
69
74
  return self._broadcast_with_answer(channel, params, timeout)
70
75
  else:
@@ -85,7 +90,8 @@ class RedisBroadcaster(interface.BaseBroadcaster):
85
90
  cond.notify()
86
91
 
87
92
  answer_channel = self._get_channel(channel) + "".join(
88
- random.choice(string.ascii_uppercase + string.digits) for _ in range(10) # nosec
93
+ random.choice(string.ascii_uppercase + string.digits) # noqa: S311
94
+ for _ in range(10)
89
95
  )
90
96
  _LOG.debug("Subscribing for broadcast answers on %s", answer_channel)
91
97
  self._pub_sub.subscribe(**{answer_channel: callback})
@@ -98,7 +104,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
98
104
  with cond:
99
105
  while len(answers) < nb_received:
100
106
  to_wait = timeout_time - time.perf_counter()
101
- if to_wait <= 0.0: # pragma: no cover
107
+ if to_wait <= 0.0:
102
108
  _LOG.warning(
103
109
  "timeout waiting for %d/%d answers on %s",
104
110
  len(answers),
@@ -16,9 +16,11 @@ class Filter:
16
16
  """
17
17
 
18
18
  def __init__(self, application: Callable[[dict[str, str], Any], Any]):
19
+ """Initialize the filter."""
19
20
  self._application = application
20
21
 
21
22
  def __call__(self, environ: dict[str, str], start_response: Any) -> Any:
23
+ """Update the environ with the headers."""
22
24
  # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
23
25
  if "HTTP_FORWARDED" in environ:
24
26
  _handle_forwarded(environ)
@@ -10,7 +10,7 @@ _LOG = logging.getLogger(__name__)
10
10
 
11
11
  def init() -> None:
12
12
  """Initialize the code coverage, for backward compatibility."""
13
- warnings.warn("init function is deprecated; use includeme instead")
13
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
14
14
  includeme()
15
15
 
16
16
 
@@ -22,7 +22,7 @@ def includeme(config: Optional[pyramid.config.Configurator] = None) -> None:
22
22
  import coverage # pylint: disable=import-outside-toplevel
23
23
 
24
24
  _LOG.warning("Setting up code coverage")
25
- report_dir = "/tmp/coverage/api" # nosec
25
+ report_dir = "/tmp/coverage/api" # noqa: S108
26
26
  os.makedirs(report_dir, exist_ok=True)
27
27
  cov = coverage.Coverage(
28
28
  data_file=os.path.join(report_dir, "coverage"),
c2cwsgiutils/db.py CHANGED
@@ -66,8 +66,11 @@ def setup_session(
66
66
  force_slave: The method/paths that needs to use the slave
67
67
 
68
68
  Returns: The SQLAlchemy session, the R/W engine and the R/O engine
69
+
69
70
  """
70
- warnings.warn("setup_session function is deprecated; use init and request.dbsession instead")
71
+ warnings.warn(
72
+ "setup_session function is deprecated; use init and request.dbsession instead", stacklevel=2
73
+ )
71
74
  if slave_prefix is None:
72
75
  slave_prefix = master_prefix
73
76
  settings = config.registry.settings
@@ -122,8 +125,11 @@ def create_session(
122
125
  engine_config: The rest of the parameters are passed as is to the sqlalchemy.create_engine function
123
126
 
124
127
  Returns: The SQLAlchemy session
128
+
125
129
  """
126
- warnings.warn("create_session function is deprecated; use init and request.dbsession instead")
130
+ warnings.warn(
131
+ "create_session function is deprecated; use init and request.dbsession instead", stacklevel=2
132
+ )
127
133
  if slave_url is None:
128
134
  slave_url = url
129
135
 
@@ -215,6 +221,7 @@ class SessionFactory(_sessionmaker):
215
221
  ro_engine: sqlalchemy.engine.Engine,
216
222
  rw_engine: sqlalchemy.engine.Engine,
217
223
  ):
224
+ """Initialize the session factory."""
218
225
  super().__init__()
219
226
  self.master_paths: Iterable[Pattern[str]] = (
220
227
  list(map(_RE_COMPILE, force_master)) if force_master else []
@@ -232,6 +239,7 @@ class SessionFactory(_sessionmaker):
232
239
  def __call__( # type: ignore
233
240
  self, request: Optional[pyramid.request.Request], readwrite: Optional[bool] = None, **local_kw: Any
234
241
  ) -> _scoped_session:
242
+ """Set the engine based on the request."""
235
243
  if readwrite is not None:
236
244
  if readwrite and not FORCE_READONLY:
237
245
  _LOG.debug("Using %s database", self.rw_engine.c2c_name) # type: ignore
@@ -374,6 +382,7 @@ def init(
374
382
  force_slave: The method/paths that needs to use the slave
375
383
 
376
384
  Returns: The SQLAlchemy session
385
+
377
386
  """
378
387
  settings = config.get_settings()
379
388
  settings["tm.manager_hook"] = "pyramid_tm.explicit_manager"
@@ -15,7 +15,7 @@ _REDIS_PREFIX = "c2c_db_maintenance_"
15
15
 
16
16
  def install_subscriber(config: pyramid.config.Configurator) -> None:
17
17
  """Install the view to configure the loggers, if configured to do so, for backward compatibility."""
18
- warnings.warn("install_subscriber function is deprecated; use includeme instead")
18
+ warnings.warn("install_subscriber function is deprecated; use includeme instead", stacklevel=2)
19
19
  includeme(config)
20
20
 
21
21
 
@@ -16,7 +16,7 @@ dump_memory_maps = utils.dump_memory_maps
16
16
 
17
17
  def init(config: pyramid.config.Configurator) -> None:
18
18
  """Initialize the debug tools, for backward compatibility."""
19
- warnings.warn("init function is deprecated; use includeme instead")
19
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
20
20
  includeme(config)
21
21
 
22
22
 
@@ -37,6 +37,8 @@ def init_daemon(config: Optional[pyramid.config.Configurator] = None) -> None:
37
37
  those requests.
38
38
  """
39
39
  if config_utils.env_or_config(config, ENV_KEY, CONFIG_KEY, type_=config_utils.config_bool):
40
- from c2cwsgiutils.debug import _listeners # pylint: disable=import-outside-toplevel
40
+ from c2cwsgiutils.debug import ( # pylint: disable=import-outside-toplevel
41
+ _listeners,
42
+ )
41
43
 
42
44
  _listeners.init()
@@ -1,5 +1,6 @@
1
1
  import gc
2
2
  import logging
3
+ import os
3
4
  import re
4
5
  import time
5
6
  from collections.abc import Mapping
@@ -8,6 +9,7 @@ from io import StringIO
8
9
  from typing import Any, Callable, cast
9
10
 
10
11
  import objgraph
12
+ import psutil
11
13
  import pyramid.config
12
14
  import pyramid.request
13
15
  import pyramid.response
@@ -77,7 +79,7 @@ def _dump_memory_diff(request: pyramid.request.Request) -> list[Any]:
77
79
  try:
78
80
  if request.params.get("no_warmup", "0").lower() in ("1", "true", "on"):
79
81
  request.invoke_subrequest(sub_request)
80
- except Exception: # nosec # pylint: disable=broad-except
82
+ except Exception: # pylint: disable=broad-except
81
83
  pass
82
84
 
83
85
  _LOG.debug("checking memory growth for %s", path)
@@ -87,6 +89,9 @@ def _dump_memory_diff(request: pyramid.request.Request) -> list[Any]:
87
89
  gc.collect(i)
88
90
 
89
91
  objgraph.growth(limit=limit, peak_stats=peak_stats, shortnames=False)
92
+ process = psutil.Process(os.getpid())
93
+ mem_before = process.memory_info()
94
+ start_time = time.time()
90
95
 
91
96
  response = None
92
97
  try:
@@ -96,12 +101,26 @@ def _dump_memory_diff(request: pyramid.request.Request) -> list[Any]:
96
101
  except HTTPException as ex:
97
102
  _LOG.debug("response was %s", str(ex))
98
103
 
104
+ elapsed_time = time.time() - start_time
99
105
  del response
100
106
 
101
107
  for i in range(3):
102
108
  gc.collect(i)
103
109
 
104
- return objgraph.growth(limit=limit, peak_stats=peak_stats, shortnames=False) # type: ignore
110
+ mem_after = process.memory_info()
111
+ return {
112
+ "memory.growth": {
113
+ "rss_kb": (mem_after.rss - mem_before.rss) / 1024,
114
+ "vms_kb": (mem_after.vms - mem_before.vms) / 1024,
115
+ "shared_kb": (mem_after.shared - mem_before.shared) / 1024,
116
+ "text_kb": (mem_after.text - mem_before.text) / 1024,
117
+ "lib_kb": (mem_after.lib - mem_before.lib) / 1024,
118
+ "data_kb": (mem_after.data - mem_before.data) / 1024,
119
+ "dirty_kb": (mem_after.dirty - mem_before.dirty) / 1024,
120
+ },
121
+ "elapsed_time": elapsed_time,
122
+ "objgraph.growth": objgraph.growth(limit=limit, peak_stats=peak_stats, shortnames=False), # type: ignore
123
+ }
105
124
 
106
125
 
107
126
  def _sleep(request: pyramid.request.Request) -> pyramid.response.Response:
@@ -132,8 +151,7 @@ def _headers(request: pyramid.request.Request) -> Mapping[str, Any]:
132
151
  }
133
152
  if "status" in request.params:
134
153
  raise exception_response(int(request.params["status"]), detail=result)
135
- else:
136
- return result
154
+ return result
137
155
 
138
156
 
139
157
  def _error(request: pyramid.request.Request) -> Any:
c2cwsgiutils/errors.py CHANGED
@@ -9,7 +9,12 @@ from typing import Any, Callable
9
9
  import pyramid.request
10
10
  import sqlalchemy.exc
11
11
  from cornice import cors
12
- from pyramid.httpexceptions import HTTPError, HTTPException, HTTPRedirection, HTTPSuccessful
12
+ from pyramid.httpexceptions import (
13
+ HTTPError,
14
+ HTTPException,
15
+ HTTPRedirection,
16
+ HTTPSuccessful,
17
+ )
13
18
  from webob.request import DisconnectionError
14
19
 
15
20
  from c2cwsgiutils import auth, config_utils
@@ -152,7 +157,7 @@ def _passthrough(exception: HTTPException, request: pyramid.request.Request) ->
152
157
 
153
158
  def init(config: pyramid.config.Configurator) -> None:
154
159
  """Initialize the error views, for backward compatibility."""
155
- warnings.warn("init function is deprecated; use includeme instead")
160
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
156
161
  includeme(config)
157
162
 
158
163
 
@@ -67,6 +67,7 @@ class JsonCheckException(Exception):
67
67
  """Checker exception used to add some structured content to a failure."""
68
68
 
69
69
  def __init__(self, message: str, json: Any):
70
+ """Initialize the exception."""
70
71
  super().__init__()
71
72
  self.message = message
72
73
  self.json = json
@@ -199,6 +200,7 @@ class HealthCheck:
199
200
  """
200
201
 
201
202
  def __init__(self, config: pyramid.config.Configurator) -> None:
203
+ """Initialize the health check view."""
202
204
  config.add_route(
203
205
  "c2c_health_check", config_utils.get_base_path(config) + r"/health_check", request_method="GET"
204
206
  )
@@ -235,6 +237,7 @@ class HealthCheck:
235
237
  engine_type: whether to check only the RW, RO or both engines
236
238
  rw_engin: the RW engine to use (if None, use the session one)
237
239
  ro_engin: the RO engine to use (if None, use the session one)
240
+
238
241
  """
239
242
  if query_cb is None:
240
243
  query_cb = self._at_least_one(at_least_one_model)
@@ -265,6 +268,7 @@ class HealthCheck:
265
268
  version_table: override the table name for the version
266
269
  rw_engin: the RW engine to use (if None, use the session one)
267
270
  ro_engin: the RO engine to use (if None, use the session one)
271
+
268
272
  """
269
273
  version_ = _get_alembic_version(alembic_ini_path, name)
270
274
 
@@ -287,26 +291,28 @@ class HealthCheck:
287
291
  assert version_schema
288
292
  assert version_table
289
293
  for binding in _get_bindings(self.session, EngineType.READ_AND_WRITE):
290
- with binding as binded_session:
291
- with _PROMETHEUS_DB_SUMMARY.labels(
294
+ with (
295
+ binding as binded_session,
296
+ _PROMETHEUS_DB_SUMMARY.labels(
292
297
  configuration=alembic_ini_path, connection=binding.name(), check="alembic"
293
- ).time():
294
- result = binded_session.execute(
295
- sqlalchemy.text(
296
- "SELECT version_num FROM " # nosec
297
- f"{sqlalchemy.sql.quoted_name(version_schema, True)}."
298
- f"{sqlalchemy.sql.quoted_name(version_table, True)}"
299
- )
300
- ).fetchone()
301
- assert result is not None
302
- (actual_version,) = result
303
- _PROMETHEUS_ALEMBIC_VERSION.labels(
304
- version=actual_version, name=name, configuration=alembic_ini_path
305
- ).set(1)
306
- if actual_version != version_:
307
- raise Exception( # pylint: disable=broad-exception-raised
308
- f"Invalid alembic version (db: {actual_version}, code: {version_})"
309
- )
298
+ ).time(),
299
+ ):
300
+ result = binded_session.execute(
301
+ sqlalchemy.text(
302
+ "SELECT version_num FROM " # noqa: S608
303
+ f"{sqlalchemy.sql.quoted_name(version_schema, True)}."
304
+ f"{sqlalchemy.sql.quoted_name(version_table, True)}"
305
+ )
306
+ ).fetchone()
307
+ assert result is not None
308
+ (actual_version,) = result
309
+ _PROMETHEUS_ALEMBIC_VERSION.labels(
310
+ version=actual_version, name=name, configuration=alembic_ini_path
311
+ ).set(1)
312
+ if actual_version != version_:
313
+ raise Exception( # pylint: disable=broad-exception-raised
314
+ f"Invalid alembic version (db: {actual_version}, code: {version_})"
315
+ )
310
316
  return version_
311
317
 
312
318
  self._checks.append(
@@ -321,9 +327,8 @@ class HealthCheck:
321
327
  Mapping[str, str], Callable[[pyramid.request.Request], Mapping[str, str]], None
322
328
  ] = None,
323
329
  name: Optional[str] = None,
324
- check_cb: Callable[
325
- [pyramid.request.Request, requests.Response], Any
326
- ] = lambda request, response: None,
330
+ check_cb: Callable[[pyramid.request.Request, requests.Response], Any] = lambda request,
331
+ response: None,
327
332
  timeout: float = 3,
328
333
  level: int = 1,
329
334
  ) -> None:
@@ -339,6 +344,7 @@ class HealthCheck:
339
344
  response as parameters)
340
345
  timeout: the timeout
341
346
  level: the level of the health check
347
+
342
348
  """
343
349
 
344
350
  def check(request: pyramid.request.Request) -> Any:
@@ -365,6 +371,7 @@ class HealthCheck:
365
371
  Arguments:
366
372
  name: the name of the check (defaults to url)
367
373
  level: the level of the health check
374
+
368
375
  """
369
376
 
370
377
  def check(request: pyramid.request.Request) -> Any:
@@ -413,6 +420,7 @@ class HealthCheck:
413
420
  Arguments:
414
421
  name: the name of the check (defaults to "version")
415
422
  level: the level of the health check
423
+
416
424
  """
417
425
 
418
426
  def check(request: pyramid.request.Request) -> dict[str, Any]:
@@ -441,6 +449,7 @@ class HealthCheck:
441
449
  name: the name of the check
442
450
  check_cb: the callback to call (takes the request as parameter)
443
451
  level: the level of the health check
452
+
444
453
  """
445
454
  assert name
446
455
  self._checks.append((name, check_cb, level))
@@ -453,9 +462,8 @@ class HealthCheck:
453
462
  "successes": {},
454
463
  }
455
464
  checks = None
456
- if "checks" in request.params:
457
- if request.params["checks"] != "":
458
- checks = request.params["checks"].split(",")
465
+ if "checks" in request.params and request.params["checks"] != "":
466
+ checks = request.params["checks"].split(",")
459
467
  for name, check, level in self._checks:
460
468
  if level <= max_level and (checks is None or name in checks):
461
469
  self._run_one(check, is_auth, level, name, request, results)
@@ -498,11 +506,13 @@ class HealthCheck:
498
506
  ) -> tuple[str, Callable[[pyramid.request.Request], None]]:
499
507
  def check(request: pyramid.request.Request) -> None:
500
508
  del request # unused
501
- with binding as session:
502
- with _PROMETHEUS_DB_SUMMARY.labels(
509
+ with (
510
+ binding as session,
511
+ _PROMETHEUS_DB_SUMMARY.labels(
503
512
  connection=binding.name(), check="database", configuration="<default>"
504
- ).time():
505
- return query_cb(session)
513
+ ).time(),
514
+ ):
515
+ return query_cb(session)
506
516
 
507
517
  return "db_engine_" + binding.name(), check
508
518
 
c2cwsgiutils/index.py CHANGED
@@ -501,7 +501,7 @@ def _github_logout(request: pyramid.request.Request) -> dict[str, Any]:
501
501
 
502
502
  def init(config: pyramid.config.Configurator) -> None:
503
503
  """Initialize the index page, for backward compatibility."""
504
- warnings.warn("init function is deprecated; use includeme instead")
504
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
505
505
  includeme(config)
506
506
 
507
507
 
c2cwsgiutils/loader.py CHANGED
@@ -1,4 +1,4 @@
1
- import logging
1
+ import logging.config
2
2
  from typing import Optional, cast
3
3
 
4
4
  from plaster_pastedeploy import Loader as BaseLoader
@@ -15,7 +15,7 @@ _REDIS_PREFIX = "c2c_logging_level_"
15
15
 
16
16
  def install_subscriber(config: pyramid.config.Configurator) -> None:
17
17
  """Install the view to configure the loggers, if configured to do so, for backward compatibility."""
18
- warnings.warn("install_subscriber function is deprecated; use includeme instead")
18
+ warnings.warn("install_subscriber function is deprecated; use includeme instead", stacklevel=2)
19
19
  includeme(config)
20
20
 
21
21
 
@@ -44,7 +44,7 @@ def _generate_model_graph(module: Any, base: Any) -> None:
44
44
  def _print_node(symbol: Any, interesting: set[Any]) -> None:
45
45
  print(f'{symbol.__name__} [label="{_get_table_desc(symbol)}", shape=box];')
46
46
  for parent in symbol.__bases__:
47
- if parent != object:
47
+ if parent is not object:
48
48
  if parent not in interesting:
49
49
  _print_node(parent, interesting)
50
50
  interesting.add(parent)
@@ -28,7 +28,7 @@ class _FastDumps:
28
28
 
29
29
  def init(config: pyramid.config.Configurator) -> None:
30
30
  """Initialize json and fast_json renderer, for backward compatibility."""
31
- warnings.warn("init function is deprecated; use includeme instead")
31
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
32
32
  includeme(config)
33
33
 
34
34
 
c2cwsgiutils/profiler.py CHANGED
@@ -9,6 +9,7 @@ class Profile(contextlib.ContextDecorator):
9
9
  """Used to profile a function with a decorator or with a with statement."""
10
10
 
11
11
  def __init__(self, path: str, print_number: int = 0) -> None:
12
+ """Initialize the profiler."""
12
13
  self.path = path
13
14
  self.print_number = print_number
14
15
  self.pr = cProfile.Profile()
@@ -2,6 +2,7 @@
2
2
 
3
3
  import os
4
4
  import re
5
+ import resource
5
6
  from collections.abc import Generator, Iterable
6
7
  from typing import Any, Optional, TypedDict, cast
7
8
 
@@ -15,6 +16,12 @@ import pyramid.config
15
16
  from c2cwsgiutils import broadcast, redis_utils
16
17
  from c2cwsgiutils.debug.utils import dump_memory_maps
17
18
 
19
+ PSUTILS = True
20
+ try:
21
+ import psutil
22
+ except ImportError:
23
+ PSUTILS = False
24
+
18
25
  _NUMBER_RE = re.compile(r"^[0-9]+$")
19
26
  MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS = [
20
27
  "c2cwsgiutils_prometheus_collector_gc",
@@ -22,12 +29,21 @@ MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS = [
22
29
  ]
23
30
 
24
31
 
32
+ def start_single_process() -> None:
33
+ """Start separate HTTP server to provide the Prometheus metrics."""
34
+ if os.environ.get("C2C_PROMETHEUS_PORT") is not None:
35
+ prometheus_client.REGISTRY.register(ResourceCollector())
36
+ prometheus_client.REGISTRY.register(MemoryInfoCollector())
37
+ prometheus_client.start_http_server(int(os.environ["C2C_PROMETHEUS_PORT"]))
38
+
39
+
25
40
  def start(registry: Optional[prometheus_client.CollectorRegistry] = None) -> None:
26
41
  """Start separate HTTP server to provide the Prometheus metrics."""
27
42
  if os.environ.get("C2C_PROMETHEUS_PORT") is not None:
28
43
  broadcast.includeme()
29
44
 
30
45
  registry = prometheus_client.CollectorRegistry() if registry is None else registry
46
+ registry.register(ResourceCollector())
31
47
  registry.register(MemoryMapCollector())
32
48
  registry.register(prometheus_client.PLATFORM_COLLECTOR)
33
49
  registry.register(MultiProcessCustomCollector())
@@ -115,6 +131,7 @@ class MultiProcessCustomCollector(prometheus_client.registry.Collector):
115
131
  """Get the metrics from the custom collectors."""
116
132
 
117
133
  def collect(self) -> Generator[prometheus_client.core.Metric, None, None]:
134
+ """Get the metrics from the custom collectors."""
118
135
  results: list[list[SerializedMetric]] = []
119
136
  for channel in MULTI_PROCESS_COLLECTOR_BROADCAST_CHANNELS:
120
137
  result = broadcast.broadcast(channel, expect_answers=True)
@@ -159,6 +176,7 @@ class MemoryMapCollector(prometheus_client.registry.Collector):
159
176
  Arguments:
160
177
  memory_type: can be rss, pss or size
161
178
  pids: the list of pids or none
179
+
162
180
  """
163
181
  super().__init__()
164
182
  self.memory_type = memory_type
@@ -179,3 +197,43 @@ class MemoryMapCollector(prometheus_client.registry.Collector):
179
197
  for e in dump_memory_maps(pid):
180
198
  gauge.add_metric([pid, e["name"]], e[self.memory_type + "_kb"] * 1024)
181
199
  yield gauge
200
+
201
+
202
+ class ResourceCollector(prometheus_client.registry.Collector):
203
+ """Collect the resources used by Python."""
204
+
205
+ def collect(self) -> Generator[prometheus_client.core.GaugeMetricFamily, None, None]:
206
+ """Get the gauge from smap file."""
207
+ gauge = prometheus_client.core.GaugeMetricFamily(
208
+ build_metric_name("python_resource"),
209
+ "Python resources",
210
+ labels=["name"],
211
+ )
212
+ r = resource.getrusage(resource.RUSAGE_SELF)
213
+ for field in dir(r):
214
+ if field.startswith("ru_"):
215
+ gauge.add_metric([field[3:]], getattr(r, field))
216
+ yield gauge
217
+
218
+
219
+ class MemoryInfoCollector(prometheus_client.registry.Collector):
220
+ """Collect the resources used by Python."""
221
+
222
+ process = psutil.Process(os.getpid())
223
+
224
+ def collect(self) -> Generator[prometheus_client.core.GaugeMetricFamily, None, None]:
225
+ """Get the gauge from smap file."""
226
+ gauge = prometheus_client.core.GaugeMetricFamily(
227
+ build_metric_name("python_memory_info"),
228
+ "Python memory info",
229
+ labels=["name"],
230
+ )
231
+ memory_info = self.process.memory_info()
232
+ gauge.add_metric(["rss"], memory_info.rss)
233
+ gauge.add_metric(["vms"], memory_info.vms)
234
+ gauge.add_metric(["shared"], memory_info.shared)
235
+ gauge.add_metric(["text"], memory_info.text)
236
+ gauge.add_metric(["lib"], memory_info.lib)
237
+ gauge.add_metric(["data"], memory_info.data)
238
+ gauge.add_metric(["dirty"], memory_info.dirty)
239
+ yield gauge
c2cwsgiutils/pyramid.py CHANGED
@@ -31,6 +31,7 @@ def includeme(config: pyramid.config.Configurator) -> None:
31
31
 
32
32
  Arguments:
33
33
  config: The pyramid Configuration
34
+
34
35
  """
35
36
  logging.captureWarnings(True)
36
37
  config.include(coverage_setup.includeme)