c2cwsgiutils 6.2.0.dev35__py3-none-any.whl → 6.2.0.dev39__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 +13 -13
  5. c2cwsgiutils/acceptance/package.json +1 -1
  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 +2 -3
  18. c2cwsgiutils/errors.py +7 -2
  19. c2cwsgiutils/health_check.py +39 -29
  20. c2cwsgiutils/index.py +1 -1
  21. c2cwsgiutils/loader.py +20 -2
  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.2.0.dev35.dist-info → c2cwsgiutils-6.2.0.dev39.dist-info}/METADATA +73 -12
  44. c2cwsgiutils-6.2.0.dev39.dist-info/RECORD +67 -0
  45. c2cwsgiutils-6.2.0.dev35.dist-info/RECORD +0 -67
  46. {c2cwsgiutils-6.2.0.dev35.dist-info → c2cwsgiutils-6.2.0.dev39.dist-info}/LICENSE +0 -0
  47. {c2cwsgiutils-6.2.0.dev35.dist-info → c2cwsgiutils-6.2.0.dev39.dist-info}/WHEEL +0 -0
  48. {c2cwsgiutils-6.2.0.dev35.dist-info → c2cwsgiutils-6.2.0.dev39.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/loader.py CHANGED
@@ -1,9 +1,9 @@
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
5
5
 
6
- from c2cwsgiutils import get_config_defaults
6
+ from c2cwsgiutils import get_config_defaults, get_logconfig_dict
7
7
 
8
8
  _LOG = logging.getLogger(__name__)
9
9
 
@@ -19,3 +19,21 @@ class Loader(BaseLoader): # type: ignore
19
19
  def __repr__(self) -> str:
20
20
  """Get the object representation."""
21
21
  return f'c2cwsgiutils.loader.Loader(uri="{self.uri}")'
22
+
23
+ def setup_logging(self, defaults: Optional[dict[str, str]] = None) -> None:
24
+ """
25
+ Set up logging via :func:`logging.config.fileConfig`.
26
+
27
+ Defaults are specified for the special ``__file__`` and ``here``
28
+ variables, similar to PasteDeploy config loading. Extra defaults can
29
+ optionally be specified as a dict in ``defaults``.
30
+
31
+ Arguments:
32
+ defaults: The defaults that will be used when passed to
33
+ :func:`logging.config.fileConfig`.
34
+
35
+ """
36
+ if "loggers" in self.get_sections():
37
+ logging.config.dictConfig(get_logconfig_dict(self.uri.path))
38
+ else:
39
+ logging.basicConfig()
@@ -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)
@@ -87,10 +87,12 @@ class PyramidCeeSysLogHandler(cee_syslog_handler.CeeSysLogHandler): # type: ign
87
87
  """A CEE (JSON format) log handler with additional information about the current request."""
88
88
 
89
89
  def __init__(self, *args: Any, **kargv: Any) -> None:
90
+ """Initialize the handler."""
90
91
  super().__init__(*args, **kargv)
91
92
  self.addFilter(_PYRAMID_FILTER)
92
93
 
93
94
  def format(self, record: Any) -> str:
95
+ """Format the record into a CEE string."""
94
96
  message = _make_message_dict(
95
97
  record,
96
98
  self._fqdn,
@@ -112,11 +114,13 @@ class JsonLogHandler(Base):
112
114
  """Log to stdout in JSON."""
113
115
 
114
116
  def __init__(self, stream: Optional[TextIO] = None):
117
+ """Initialize the handler."""
115
118
  super().__init__(stream)
116
119
  self.addFilter(_PYRAMID_FILTER)
117
120
  self._fqdn = socket.getfqdn()
118
121
 
119
122
  def format(self, record: Any) -> str:
123
+ """Format the record into a JSON string."""
120
124
  message = _make_message_dict(
121
125
  record, self._fqdn, debugging_fields=True, extra_fields=True, facility=None, static_fields={}
122
126
  )
@@ -26,7 +26,7 @@ def _execute_command_patch(self: Any, command: str, *args: Any, **options: Any)
26
26
 
27
27
  def init(config: Optional[pyramid.config.Configurator] = None) -> None:
28
28
  """Initialize the Redis tracking, for backward compatibility."""
29
- warnings.warn("init function is deprecated; use includeme instead")
29
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
30
30
  includeme(config)
31
31
 
32
32
 
@@ -100,11 +100,13 @@ class PubSubWorkerThread(threading.Thread):
100
100
  """A clone of redis.client.PubSubWorkerThread that doesn't die when the connections are broken."""
101
101
 
102
102
  def __init__(self, pubsub: redis.client.PubSub, name: Optional[str] = None) -> None:
103
+ """Initialize the PubSubWorkerThread."""
103
104
  super().__init__(name=name, daemon=True)
104
105
  self.pubsub = pubsub
105
106
  self._running = False
106
107
 
107
108
  def run(self) -> None:
109
+ """Run the worker."""
108
110
  if self._running:
109
111
  return
110
112
  self._running = True
@@ -83,7 +83,7 @@ def _patch_requests() -> None:
83
83
 
84
84
  def init(config: Optional[pyramid.config.Configurator] = None) -> None:
85
85
  """Initialize the request tracking, for backward compatibility."""
86
- warnings.warn("init function is deprecated; use includeme instead")
86
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
87
87
  includeme(config)
88
88
 
89
89
 
@@ -24,7 +24,7 @@ def _get_package_version(comp: str) -> tuple[Optional[str], Optional[str]]:
24
24
  if matcher:
25
25
  return cast(tuple[str, str], matcher.groups())
26
26
  else:
27
- if len(comp) > 0 and not comp[:3] == "-e ":
27
+ if len(comp) > 0 and comp[:3] != "-e ":
28
28
  print("Cannot parse package version: " + comp)
29
29
  return None, None
30
30
 
@@ -46,7 +46,9 @@ def _get_packages_version() -> dict[str, str]:
46
46
 
47
47
  def deprecated() -> None:
48
48
  """Run the command and print a deprecated notice."""
49
- warnings.warn("c2cwsgiutils_genversion.py is deprecated; use c2cwsgiutils-genversion instead")
49
+ warnings.warn(
50
+ "c2cwsgiutils_genversion.py is deprecated; use c2cwsgiutils-genversion instead", stacklevel=2
51
+ )
50
52
  return main()
51
53
 
52
54
 
@@ -62,6 +62,7 @@ class Reporter:
62
62
  """The stats reporter."""
63
63
 
64
64
  def __init__(self, args: argparse.Namespace) -> None:
65
+ """Initialize the reporter."""
65
66
  self._error: Optional[Exception] = None
66
67
  self.registry = CollectorRegistry()
67
68
  self.prometheus_push = args.prometheus_url is not None
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """Test a MapfishPrint server."""
3
+
3
4
  import argparse
4
5
  import logging
5
6
  import pprint
@@ -23,7 +24,9 @@ def _parse_args() -> argparse.Namespace:
23
24
 
24
25
  def deprecated() -> None:
25
26
  """Run the command and print a deprecated notice."""
26
- warnings.warn("c2cwsgiutils_test_print.py is deprecated; use c2cwsgiutils-test-print instead")
27
+ warnings.warn(
28
+ "c2cwsgiutils_test_print.py is deprecated; use c2cwsgiutils-test-print instead", stacklevel=2
29
+ )
27
30
  return main()
28
31
 
29
32
 
c2cwsgiutils/sentry.py CHANGED
@@ -33,7 +33,7 @@ def _create_before_send_filter(tags: MutableMapping[str, str]) -> Callable[[Any,
33
33
 
34
34
  def init(config: Optional[pyramid.config.Configurator] = None) -> None:
35
35
  """Initialize the Sentry integration, for backward compatibility."""
36
- warnings.warn("init function is deprecated; use includeme instead")
36
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
37
37
  includeme(config)
38
38
 
39
39
 
@@ -5,6 +5,7 @@ Must be imported at the very beginning of the process's life, before any other m
5
5
  """
6
6
 
7
7
  import argparse
8
+ import logging
8
9
  import warnings
9
10
  from typing import Any, Callable, Optional, TypedDict, cast
10
11
 
@@ -55,7 +56,9 @@ def init(config_file: str = "c2c:///app/production.ini") -> None:
55
56
 
56
57
  def init_logging(config_file: str = "c2c:///app/production.ini") -> None:
57
58
  """Initialize the non-WSGI application."""
58
- warnings.warn("init_logging function is deprecated; use init instead so that all features are enabled")
59
+ warnings.warn(
60
+ "init_logging function is deprecated; use init instead so that all features are enabled", stacklevel=2
61
+ )
59
62
  loader = get_config_loader(config_file)
60
63
  loader.setup_logging(None)
61
64
 
@@ -95,4 +98,5 @@ def bootstrap_application(
95
98
  """
96
99
  loader = get_config_loader(config_uri)
97
100
  loader.setup_logging(options)
101
+ logging.getLogger(__name__).info("Loading the application from %s", config_uri)
98
102
  return cast(PyramidEnv, bootstrap(config_uri, options=options))
@@ -18,7 +18,7 @@ _LOG = logging.getLogger(__name__)
18
18
 
19
19
  def init(config: pyramid.config.Configurator) -> None:
20
20
  """Install a pyramid event handler that adds the request information, for backward compatibility."""
21
- warnings.warn("init function is deprecated; use includeme instead")
21
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
22
22
  includeme(config)
23
23
 
24
24
 
@@ -56,7 +56,7 @@ class _Repository:
56
56
  ]
57
57
  )
58
58
  _LOG.info(output)
59
- except Exception: # nosec # pylint: disable=broad-except
59
+ except Exception: # pylint: disable=broad-except # noqa: S110
60
60
  pass
61
61
 
62
62
 
@@ -11,7 +11,10 @@ from sqlalchemy.exc import SQLAlchemyError
11
11
  from sqlalchemy.orm import sessionmaker
12
12
  from sqlalchemy_utils import create_database, database_exists
13
13
 
14
- from c2cwsgiutils.sqlalchemylogger._filters import ContainsExpression, DoesNotContainExpression
14
+ from c2cwsgiutils.sqlalchemylogger._filters import (
15
+ ContainsExpression,
16
+ DoesNotContainExpression,
17
+ )
15
18
  from c2cwsgiutils.sqlalchemylogger._models import Base, create_log_class
16
19
 
17
20
  _LOG = logging.getLogger(__name__)
@@ -29,12 +32,13 @@ class SQLAlchemyHandler(logging.Handler):
29
32
  does_not_contain_expression: str = "",
30
33
  contains_expression: str = "",
31
34
  ) -> None:
35
+ """Initialize the SQLAlchemyHandler."""
32
36
  super().__init__()
33
37
  # Initialize DB session
34
38
  self.engine = create_engine(sqlalchemy_url["url"])
35
39
  self.Log = create_log_class( # pylint: disable=invalid-name
36
40
  tablename=sqlalchemy_url.get("tablename", "logs"),
37
- tableargs=sqlalchemy_url.get("tableargs", None), # type: ignore
41
+ tableargs=sqlalchemy_url.get("tableargs"), # type: ignore
38
42
  )
39
43
  Base.metadata.bind = self.engine
40
44
  self.session = sessionmaker(bind=self.engine)() # noqa
@@ -61,15 +65,16 @@ class SQLAlchemyHandler(logging.Handler):
61
65
  if not self.log_queue.empty():
62
66
  logs.append(self.log_queue.get())
63
67
  self.log_queue.task_done()
64
- if logs:
65
- # try to reduce the number of INSERT requests to the DB
66
- # by writing chunks of self.MAX_NB_LOGS size,
67
- # but also do not wait forever before writing stuff (self.MAX_TIMOUT)
68
- if (len(logs) >= self.MAX_NB_LOGS) or (
69
- time.perf_counter() >= (time_since_last + self.MAX_TIMEOUT)
70
- ):
71
- self._write_logs(logs)
72
- break
68
+ # try to reduce the number of INSERT requests to the DB
69
+ # by writing chunks of self.MAX_NB_LOGS size,
70
+ # but also do not wait forever before writing stuff (self.MAX_TIMOUT)
71
+ if (
72
+ logs
73
+ and (len(logs) >= self.MAX_NB_LOGS)
74
+ or (time.perf_counter() >= (time_since_last + self.MAX_TIMEOUT))
75
+ ):
76
+ self._write_logs(logs)
77
+ break
73
78
  _LOG.debug("%s: stopping processor thread", __name__)
74
79
 
75
80
  def _write_logs(self, logs: list[Any]) -> None:
@@ -94,7 +99,7 @@ class SQLAlchemyHandler(logging.Handler):
94
99
  _LOG.info("%s: creating new database", __name__)
95
100
  if not database_exists(self.engine.url):
96
101
  create_database(self.engine.url)
97
- # FIXME: we should not access directly the private __table_args__
102
+ # FIXME: we should not access directly the private __table_args__ # pylint: disable=fixme
98
103
  # variable, but add an accessor method in models.Log class
99
104
  if not isinstance(self.Log.__table_args__, type(None)) and self.Log.__table_args__.get(
100
105
  "schema", None
@@ -107,6 +112,7 @@ class SQLAlchemyHandler(logging.Handler):
107
112
  Base.metadata.create_all(self.engine)
108
113
 
109
114
  def emit(self, record: Any) -> None:
115
+ """Emit the log."""
110
116
  trace = None
111
117
  exc = record.__dict__["exc_info"]
112
118
  if exc:
@@ -10,7 +10,7 @@ from c2cwsgiutils.stats_pyramid import _pyramid_spy
10
10
 
11
11
  def init(config: pyramid.config.Configurator) -> None:
12
12
  """Initialize the whole stats module, 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(config)
15
15
 
16
16
 
@@ -20,6 +20,7 @@ def includeme(config: pyramid.config.Configurator) -> None:
20
20
 
21
21
  Arguments:
22
22
  config: The Pyramid config
23
+
23
24
  """
24
25
  _pyramid_spy.init(config)
25
26
  init_db_spy()
@@ -99,6 +99,7 @@ def init(config: pyramid.config.Configurator) -> None: # pragma: nocover
99
99
 
100
100
  Arguments:
101
101
  config: The Pyramid config
102
+
102
103
  """
103
104
  config.add_subscriber(_request_callback, pyramid.events.NewRequest)
104
105
  config.add_subscriber(_before_rendered_callback, pyramid.events.BeforeRender)
c2cwsgiutils/version.py CHANGED
@@ -37,7 +37,7 @@ _PROMETHEUS_VERSIONS_INFO = prometheus_client.Gauge(
37
37
 
38
38
  def init(config: pyramid.config.Configurator) -> None:
39
39
  """Initialize the versions view, for backward compatibility."""
40
- warnings.warn("init function is deprecated; use includeme instead")
40
+ warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
41
41
  includeme(config)
42
42
 
43
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: c2cwsgiutils
3
- Version: 6.2.0.dev35
3
+ Version: 6.2.0.dev39
4
4
  Summary: Common utilities for Camptocamp WSGI applications
5
5
  Home-page: https://github.com/camptocamp/c2cwsgiutils
6
6
  License: BSD-2-Clause
@@ -33,23 +33,26 @@ Provides-Extra: sentry
33
33
  Provides-Extra: standard
34
34
  Provides-Extra: test-images
35
35
  Provides-Extra: tests
36
+ Provides-Extra: waitress
36
37
  Provides-Extra: webserver
37
- Requires-Dist: SQLAlchemy ; extra == "standard" or extra == "webserver" or extra == "all"
38
- Requires-Dist: SQLAlchemy-Utils ; extra == "standard" or extra == "webserver" or extra == "all"
38
+ Requires-Dist: Paste ; extra == "standard" or extra == "waitress" or extra == "all"
39
+ Requires-Dist: SQLAlchemy ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
40
+ Requires-Dist: SQLAlchemy-Utils ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
39
41
  Requires-Dist: alembic ; extra == "standard" or extra == "alembic" or extra == "all"
40
42
  Requires-Dist: boltons ; extra == "tests" or extra == "all"
41
43
  Requires-Dist: cee_syslog_handler
42
- Requires-Dist: cornice ; extra == "standard" or extra == "webserver" or extra == "all"
44
+ Requires-Dist: cornice ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
45
+ Requires-Dist: coverage ; extra == "debug" or extra == "all"
43
46
  Requires-Dist: gunicorn ; extra == "standard" or extra == "webserver" or extra == "all"
44
47
  Requires-Dist: lxml ; extra == "tests" or extra == "all"
45
48
  Requires-Dist: objgraph ; extra == "debug" or extra == "all"
46
- Requires-Dist: prometheus-client ; extra == "standard" or extra == "webserver" or extra == "all"
49
+ Requires-Dist: prometheus-client ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
47
50
  Requires-Dist: psutil ; extra == "debug" or extra == "all"
48
- Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "all"
51
+ Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
49
52
  Requires-Dist: pyjwt ; extra == "standard" or extra == "oauth2" or extra == "all"
50
- Requires-Dist: pyramid ; extra == "standard" or extra == "webserver" or extra == "all"
51
- Requires-Dist: pyramid-tm ; extra == "standard" or extra == "webserver" or extra == "all"
52
- Requires-Dist: pyramid_mako ; extra == "standard" or extra == "webserver" or extra == "all"
53
+ Requires-Dist: pyramid ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
54
+ Requires-Dist: pyramid-tm ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
55
+ Requires-Dist: pyramid_mako ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
53
56
  Requires-Dist: pyyaml
54
57
  Requires-Dist: redis ; extra == "standard" or extra == "broadcast" or extra == "all"
55
58
  Requires-Dist: requests
@@ -57,9 +60,9 @@ Requires-Dist: requests-oauthlib ; extra == "standard" or extra == "oauth2" or e
57
60
  Requires-Dist: scikit-image ; extra == "test-images"
58
61
  Requires-Dist: sentry-sdk ; extra == "standard" or extra == "sentry" or extra == "all"
59
62
  Requires-Dist: ujson
60
- Requires-Dist: waitress ; extra == "dev" or extra == "all"
61
- Requires-Dist: zope.interface ; extra == "standard" or extra == "webserver" or extra == "all"
62
- Requires-Dist: zope.sqlalchemy ; extra == "standard" or extra == "webserver" or extra == "all"
63
+ Requires-Dist: waitress ; extra == "standard" or extra == "dev" or extra == "waitress" or extra == "all"
64
+ Requires-Dist: zope.interface ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
65
+ Requires-Dist: zope.sqlalchemy ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
63
66
  Project-URL: Repository, https://github.com/camptocamp/c2cwsgiutils
64
67
  Description-Content-Type: text/markdown
65
68
 
@@ -673,6 +676,64 @@ def hello_get(request):
673
676
  return {'hello': True}
674
677
  ```
675
678
 
679
+ ## Waitress
680
+
681
+ In production mode we usually use Gunicorn but we can also use Waitress.
682
+
683
+ The advantage to use Waitress is that it creates only one process, that makes it easier to manage especially on Kubernetes:
684
+
685
+ - The memory is more stable.
686
+ - The OOM killer will restart the container.
687
+ - Prometheus didn't request trick to aggregate the metrics.
688
+
689
+ Then to migrate from Gunicorn to Waitress you should do:
690
+
691
+ Add call to `c2cwsgiutils.prometheus.start_single_process()` on your application main function.
692
+
693
+ Changes to do in your docker file:
694
+
695
+ ```diff
696
+
697
+ ENV \
698
+ - GUNICORN_LOG_LEVEL=WARNING \
699
+ + WAITRESS_LOG_LEVEL=WARNING \
700
+ + WAITRESS_THREADS=10 \
701
+
702
+ -RUN mkdir -p /prometheus-metrics \
703
+ - && chmod a+rwx /prometheus-metrics
704
+ -ENV PROMETHEUS_MULTIPROC_DIR=/prometheus-metrics
705
+
706
+
707
+ -CMD ["/venv/bin/gunicorn", "--paste=/app/production.ini"]
708
+ +CMD ["/venv/bin/pserve", "c2c:///app/production.ini"]
709
+ ```
710
+
711
+ Remove the no more needed file `gunicorn.conf.py`.
712
+
713
+ Update the `production.ini` file:
714
+
715
+ ```diff
716
+
717
+ -# this file should be used by gunicorn.
718
+
719
+ [server:main]
720
+ +threads = %(WAITRESS_THREADS)s
721
+ +trusted_proxy = True
722
+ +clear_untrusted_proxy_headers = False
723
+
724
+ [loggers]
725
+ -keys = root, gunicorn, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
726
+ +keys = root, waitress, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
727
+
728
+ -[logger_gunicorn]
729
+ -level = %(GUNICORN_LOG_LEVEL)s
730
+ +[logger_waitress]
731
+ +level = %(WAITRESS_LOG_LEVEL)s
732
+ handlers =
733
+ -qualname = gunicorn.error
734
+ +qualname = waitress
735
+ ```
736
+
676
737
  # Exception handling
677
738
 
678
739
  c2cwsgiutils can install exception handling views that will catch any exception raised by the
@@ -0,0 +1,67 @@
1
+ c2cwsgiutils/__init__.py,sha256=HVSc-4O8i2aB0ozEI4AI8Xsb-4S6fAwhl8uRhv-DsFg,4057
2
+ c2cwsgiutils/acceptance/__init__.py,sha256=5PeqPHxAPMRbZpKk1XlC9RCyotegSsKVfPxADYAgG7w,1502
3
+ c2cwsgiutils/acceptance/connection.py,sha256=A83v335e4s-_cePe-bqRcEHHTOdgguetzdAtVp19-Zk,9834
4
+ c2cwsgiutils/acceptance/image.py,sha256=Tr_cdtQL6WhRq0qIS0P6ulLDwOprbcE1jc1Ft1Qi4OY,8956
5
+ c2cwsgiutils/acceptance/package-lock.json,sha256=P9o-SpVVOwVPzXZy8Tglwkd3xQTQR6eO6XjVRdWw7C4,44497
6
+ c2cwsgiutils/acceptance/package.json,sha256=WEN1DpMNxNroBVG4wExr2Nmf9jyURyZhhGbckrVeoVU,101
7
+ c2cwsgiutils/acceptance/print.py,sha256=DOPDHUP9wfM75qiPYhFI1P3awQKIBH-edO_TmhCjT8k,2568
8
+ c2cwsgiutils/acceptance/screenshot.js,sha256=FAJYIWOLJFMm0MNggKzo3mIybtN-VtKLdMzPhQ9pO1g,2041
9
+ c2cwsgiutils/acceptance/utils.py,sha256=beq_pGJCMBRMmzLERw7z79vc9KtX5gY-g37WiUHa4q8,1901
10
+ c2cwsgiutils/auth.py,sha256=gNJ8VKXPJu_lSlOwxhNSrjjaJ9TzasWIbEpkmYnoauQ,9582
11
+ c2cwsgiutils/broadcast/__init__.py,sha256=9vhFSNUwMqNp73D3LXnQJeLOYB49VUU4KwIoOvDaS3o,4484
12
+ c2cwsgiutils/broadcast/interface.py,sha256=AkDT_m0eXTQp3mmMLf9KRtpXnWoVhJpb_UNjCbHM-3I,665
13
+ c2cwsgiutils/broadcast/local.py,sha256=BZ0iDmU81W8C3Xzs9Dz9jRdTO-WXXvA_7Z5xV6RWoNA,1317
14
+ c2cwsgiutils/broadcast/redis.py,sha256=9FsKxsddf9khYe7aTKou2lmLgXEVcRqVyeFuhxM8-ZM,5378
15
+ c2cwsgiutils/broadcast/utils.py,sha256=0fQZXPu3p_5LEJpGenJwiiMxECQjJhjZBjIkBk8h-ng,272
16
+ c2cwsgiutils/client_info.py,sha256=MIlD88JJ-IL3GwhJsMB73IbsWcglMkdnCyOgDEQG8ns,4004
17
+ c2cwsgiutils/config_utils.py,sha256=vkBu-3GQsE94NOBOvT5FE-Ij29EUrKnDsmdUdtu_yzo,1524
18
+ c2cwsgiutils/coverage_setup.py,sha256=OGsz3bDX8Vcy9K8TBkRb4aQVAhK6618OVV9Az2p3hGA,928
19
+ c2cwsgiutils/db.py,sha256=YvJKo1cXhKcjYwRpUkQbx70YOnOgsYJ5wID5rkLhF6g,16262
20
+ c2cwsgiutils/db_maintenance_view.py,sha256=1y_sfjnHsvl98yzTM24rqmchqGIXDp61KTwraXJZM0s,3102
21
+ c2cwsgiutils/debug/__init__.py,sha256=a2RytL9_DgqiJzc5CCdKd1yF2sSk6SCIVh1gUrWVq1o,1364
22
+ c2cwsgiutils/debug/_listeners.py,sha256=-fk3KFeB_E4m_YFXJ5MfxuqfA1sWCje9p3FH_93WfXM,4248
23
+ c2cwsgiutils/debug/_views.py,sha256=bge_jnylY6oBtWTZjSoDByLbx3MjapY4JiZu97xQoWU,8518
24
+ c2cwsgiutils/debug/utils.py,sha256=sIKZHQ8empzxE2OI3h7Uce8cvv4O7AD1y_VYeZfLVCA,2320
25
+ c2cwsgiutils/errors.py,sha256=miNVnEX9qBtwaRnny_k9IcBbbw-8vC3unFumi2w5Q9w,6679
26
+ c2cwsgiutils/health_check.py,sha256=Wi3JQCKa7tqACuSgBi13oE69Xc-Vz1nheqBaxfOGXQU,20141
27
+ c2cwsgiutils/index.py,sha256=ag7A8HJ9MgU72etDBuFMaci-xKjOD9R8OOm3UMEoQJY,18002
28
+ c2cwsgiutils/loader.py,sha256=Hl9q-Ts-kXHLkV5PWFoO0jPBqhU2jKoCFvgDIdJaeF4,1333
29
+ c2cwsgiutils/logging_view.py,sha256=wa2hCjGKkpRwrCA4K6Zk1Z3hPGUMB6SJAnki-_lLY00,3384
30
+ c2cwsgiutils/models_graph.py,sha256=AROTzCHJCjjOiOmQj_Hk40WQdIPaHFLgN4Jech1aUzc,2683
31
+ c2cwsgiutils/pretty_json.py,sha256=houw_oeU63CgzQTPlBrrYzinpE-UtqPeYLfTXdLHyu0,1711
32
+ c2cwsgiutils/profiler.py,sha256=3902yp1rHpSNpsYKmqs_IrEpFg9P2QqNtjyolk53ZgQ,778
33
+ c2cwsgiutils/prometheus.py,sha256=vTb0cDO30DQwjVdu0qCFEyTAOv7jeUFioDjTCbp1bYA,8712
34
+ c2cwsgiutils/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
+ c2cwsgiutils/pyramid.py,sha256=kSCsBG5IPd0-tsXwdiJ3cj_yj-AxQYFusvTxPE6AaeQ,1388
36
+ c2cwsgiutils/pyramid_logging.py,sha256=U1MD7xEAGH7QdX7FA6O12eHhzMx76i1WCXtf_EiJgVE,3911
37
+ c2cwsgiutils/redis_stats.py,sha256=yUVVNGR_rIecH4uRfRWMySERCTYMWtJhSXAs0iK4TAs,1646
38
+ c2cwsgiutils/redis_utils.py,sha256=kY1QbZWVnUI-bxCeJOxn5VBfna2hS1oUQxx9HEws_ww,4813
39
+ c2cwsgiutils/request_tracking/__init__.py,sha256=ZeCxPJNrUxKLDmV5bOs0zmD9vx0FFIPs7T3xDNG-5BU,4146
40
+ c2cwsgiutils/request_tracking/_sql.py,sha256=ZOQ3hD5z-WasfBzNig75fjrCvw4tGr1rhWOv97iLYws,572
41
+ c2cwsgiutils/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
+ c2cwsgiutils/scripts/genversion.py,sha256=SZswHR6qbeErcxn5d5yAa4QPnw9z4LCeP9GOweTj16g,2014
43
+ c2cwsgiutils/scripts/stats_db.py,sha256=EaI1WAHGKj-sUWpznIOs8yMlgx2pnzwAksA8hA-nF28,10452
44
+ c2cwsgiutils/scripts/test_print.py,sha256=pLqqVZ9cVvjdD3MdJQ7Ul8i5OIBFvo1Gnr78rdH0Dh4,2130
45
+ c2cwsgiutils/sentry.py,sha256=ZnWo0-8xgwWxDG_wo9gI8rDtZlKgcZXorG3b0tU5Hh0,6508
46
+ c2cwsgiutils/services.py,sha256=AxdiH2S8gfEQpOY36R4WqKtJ-3P4OXhx0srTm5G5070,1569
47
+ c2cwsgiutils/setup_process.py,sha256=K4o3feB98zKoPiI2k7MnTsItwf_i-NSL8ziUmGnYg_Q,3553
48
+ c2cwsgiutils/sql_profiler/__init__.py,sha256=YR8003lCNNGC2eZdyIt5gt76-1frG5szw8IYE_EsQB4,921
49
+ c2cwsgiutils/sql_profiler/_impl.py,sha256=OdhAn-5x84Bz5RCldhgjX50b6yxSLnLh5nydNgHvgGc,3801
50
+ c2cwsgiutils/sqlalchemylogger/README.md,sha256=qXfyhGC4Kbt-Ye_geTPm_1InpclhthmKZpc66rqB4as,2018
51
+ c2cwsgiutils/sqlalchemylogger/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
52
+ c2cwsgiutils/sqlalchemylogger/_filters.py,sha256=OJQ9_WA-fd9fMZ7TUNFzHHTPI6msw2NVBl5RoeYFnGw,752
53
+ c2cwsgiutils/sqlalchemylogger/_models.py,sha256=A9SQ8AqUazCMemVjp5p_1x4bZG3LAYW9pOXT84FdNkE,1471
54
+ c2cwsgiutils/sqlalchemylogger/examples/example.py,sha256=n48dJdUi1FH1hfBMAbfHLGPSb1bOVD8pXMxXB57PnpQ,460
55
+ c2cwsgiutils/sqlalchemylogger/handlers.py,sha256=hDDWmiV6ksEcRCJlqIdQW3P9hq2BTC70wrLmY0vYkyA,4979
56
+ c2cwsgiutils/static/favicon-16x16.png,sha256=LKk6RFvb3NlPIZdDfAodE8H9IN8KM6CMGnMx4vOHlUQ,887
57
+ c2cwsgiutils/static/favicon-32x32.png,sha256=i4ucx08zAZARd8e7JTMGK-gb5WcOmyuDN6IN4brsEOo,1216
58
+ c2cwsgiutils/stats_pyramid/__init__.py,sha256=0NizIUbaPbNf6bPye7pGrF9AaFerIN30EhEMts1XgfY,801
59
+ c2cwsgiutils/stats_pyramid/_db_spy.py,sha256=A61t6VKIrRRIjbyZTldmAUl_Q3ZDVFYqyxjuntzmllc,2919
60
+ c2cwsgiutils/stats_pyramid/_pyramid_spy.py,sha256=GnJRlLSAPl4PwQH5jfmc2OX5Hz9MSRRVJT2VcqkniVE,3518
61
+ c2cwsgiutils/templates/index.html.mako,sha256=cK8qGBDeQG5SiJJCfvL0oKpgacr7dPKx634AAQivmjA,1416
62
+ c2cwsgiutils/version.py,sha256=7H3URblj26Ql0bL3eXtP0LSRBeW4HbEsQ8O_BfWNr90,3124
63
+ c2cwsgiutils-6.2.0.dev39.dist-info/LICENSE,sha256=r7ueGz9Fl2Bv3rmeQy0DEtohLmAiufRaCuv6Y5fyNhE,1304
64
+ c2cwsgiutils-6.2.0.dev39.dist-info/METADATA,sha256=E3o2wdqpTiwsZqHf5FX6mzBaXeIQQruwiYS7nHwjJis,36359
65
+ c2cwsgiutils-6.2.0.dev39.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
66
+ c2cwsgiutils-6.2.0.dev39.dist-info/entry_points.txt,sha256=ujgqMTL1awN9qDg8WXmrF7m0fgR-hslUM6zKH86pvy0,703
67
+ c2cwsgiutils-6.2.0.dev39.dist-info/RECORD,,