c2cwsgiutils 5.2.1__py3-none-any.whl → 5.2.1.dev197__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 +12 -12
  2. c2cwsgiutils/acceptance/connection.py +5 -2
  3. c2cwsgiutils/acceptance/image.py +95 -3
  4. c2cwsgiutils/acceptance/package-lock.json +1933 -0
  5. c2cwsgiutils/acceptance/package.json +7 -0
  6. c2cwsgiutils/acceptance/print.py +3 -3
  7. c2cwsgiutils/acceptance/screenshot.js +62 -0
  8. c2cwsgiutils/acceptance/utils.py +14 -22
  9. c2cwsgiutils/auth.py +4 -4
  10. c2cwsgiutils/broadcast/__init__.py +15 -7
  11. c2cwsgiutils/broadcast/interface.py +3 -2
  12. c2cwsgiutils/broadcast/local.py +3 -2
  13. c2cwsgiutils/broadcast/redis.py +6 -5
  14. c2cwsgiutils/client_info.py +5 -5
  15. c2cwsgiutils/config_utils.py +2 -1
  16. c2cwsgiutils/db.py +20 -11
  17. c2cwsgiutils/db_maintenance_view.py +2 -1
  18. c2cwsgiutils/debug/_listeners.py +7 -6
  19. c2cwsgiutils/debug/_views.py +11 -10
  20. c2cwsgiutils/debug/utils.py +5 -5
  21. c2cwsgiutils/health_check.py +72 -73
  22. c2cwsgiutils/index.py +90 -105
  23. c2cwsgiutils/loader.py +3 -3
  24. c2cwsgiutils/logging_view.py +3 -2
  25. c2cwsgiutils/models_graph.py +4 -4
  26. c2cwsgiutils/prometheus.py +175 -57
  27. c2cwsgiutils/pyramid.py +4 -2
  28. c2cwsgiutils/pyramid_logging.py +2 -1
  29. c2cwsgiutils/redis_stats.py +13 -11
  30. c2cwsgiutils/redis_utils.py +11 -5
  31. c2cwsgiutils/request_tracking/__init__.py +36 -30
  32. c2cwsgiutils/scripts/genversion.py +4 -4
  33. c2cwsgiutils/scripts/stats_db.py +92 -60
  34. c2cwsgiutils/sentry.py +2 -1
  35. c2cwsgiutils/setup_process.py +12 -16
  36. c2cwsgiutils/sql_profiler/_impl.py +3 -2
  37. c2cwsgiutils/sqlalchemylogger/_models.py +2 -2
  38. c2cwsgiutils/sqlalchemylogger/handlers.py +6 -6
  39. c2cwsgiutils/static/favicon-16x16.png +0 -0
  40. c2cwsgiutils/static/favicon-32x32.png +0 -0
  41. c2cwsgiutils/stats_pyramid/__init__.py +7 -11
  42. c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
  43. c2cwsgiutils/stats_pyramid/_pyramid_spy.py +27 -21
  44. c2cwsgiutils/templates/index.html.mako +50 -0
  45. c2cwsgiutils/version.py +49 -16
  46. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +168 -99
  47. c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
  48. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -1
  49. c2cwsgiutils/acceptance/composition.py +0 -129
  50. c2cwsgiutils/metrics.py +0 -110
  51. c2cwsgiutils/scripts/check_es.py +0 -130
  52. c2cwsgiutils/stats.py +0 -344
  53. c2cwsgiutils/stats_pyramid/_views.py +0 -16
  54. c2cwsgiutils-5.2.1.dist-info/RECORD +0 -66
  55. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/LICENSE +0 -0
  56. {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/entry_points.txt +0 -0
c2cwsgiutils/__init__.py CHANGED
@@ -4,12 +4,12 @@ import os
4
4
  import re
5
5
  import sys
6
6
  from configparser import SectionProxy
7
- from typing import Any, Dict, Set
7
+ from typing import Any
8
8
 
9
9
  LOG = logging.getLogger(__name__)
10
10
 
11
11
 
12
- def get_config_defaults() -> Dict[str, str]:
12
+ def get_config_defaults() -> dict[str, str]:
13
13
  """
14
14
  Get the environment variables as defaults for configparser.
15
15
 
@@ -18,8 +18,8 @@ def get_config_defaults() -> Dict[str, str]:
18
18
 
19
19
  configparser interpretate the % then we need to escape them
20
20
  """
21
- result: Dict[str, str] = {}
22
- lowercase_keys: Set[str] = set()
21
+ result: dict[str, str] = {}
22
+ lowercase_keys: set[str] = set()
23
23
  for key, value in os.environ.items():
24
24
  if key.lower() in lowercase_keys:
25
25
  LOG.warning("The environment variable '%s' is duplicated with different case, ignoring", key)
@@ -29,9 +29,9 @@ def get_config_defaults() -> Dict[str, str]:
29
29
  return result
30
30
 
31
31
 
32
- def _create_handlers(config: configparser.ConfigParser) -> Dict[str, Any]:
32
+ def _create_handlers(config: configparser.ConfigParser) -> dict[str, Any]:
33
33
  handlers = [k.strip() for k in config["handlers"]["keys"].split(",")]
34
- d_handlers: Dict[str, Any] = {}
34
+ d_handlers: dict[str, Any] = {}
35
35
  stream_re = re.compile(r"\((.*?),\)")
36
36
  for hh in handlers:
37
37
  block = config[f"handler_{hh}"]
@@ -53,8 +53,8 @@ def _create_handlers(config: configparser.ConfigParser) -> Dict[str, Any]:
53
53
  return d_handlers
54
54
 
55
55
 
56
- def _filter_logger(block: SectionProxy) -> Dict[str, Any]:
57
- out: Dict[str, Any] = {"level": block["level"]}
56
+ def _filter_logger(block: SectionProxy) -> dict[str, Any]:
57
+ out: dict[str, Any] = {"level": block["level"]}
58
58
  handlers = block.get("handlers", "")
59
59
  if handlers != "":
60
60
  out["handlers"] = [block["handlers"]]
@@ -65,7 +65,7 @@ def _filter_logger(block: SectionProxy) -> Dict[str, Any]:
65
65
  # logging configuration
66
66
  # https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
67
67
  ###
68
- def get_logconfig_dict(filename: str) -> Dict[str, Any]:
68
+ def get_logconfig_dict(filename: str) -> dict[str, Any]:
69
69
  """
70
70
  Create a logconfig dictionary based on the provided ini file.
71
71
 
@@ -76,8 +76,8 @@ def get_logconfig_dict(filename: str) -> Dict[str, Any]:
76
76
  loggers = [k.strip() for k in config["loggers"]["keys"].split(",")]
77
77
  formatters = [k.strip() for k in config["formatters"]["keys"].split(",")]
78
78
 
79
- d_loggers: Dict[str, Any] = {}
80
- root: Dict[str, Any] = {}
79
+ d_loggers: dict[str, Any] = {}
80
+ root: dict[str, Any] = {}
81
81
  for ll in loggers:
82
82
  block = config[f"logger_{ll}"]
83
83
  if ll == "root":
@@ -86,7 +86,7 @@ def get_logconfig_dict(filename: str) -> Dict[str, Any]:
86
86
  qualname = block["qualname"]
87
87
  d_loggers[qualname] = _filter_logger(block)
88
88
 
89
- d_formatters: Dict[str, Any] = {}
89
+ d_formatters: dict[str, Any] = {}
90
90
  for ff in formatters:
91
91
  block = config[f"formatter_{ff}"]
92
92
  d_formatters[ff] = {
@@ -1,9 +1,9 @@
1
1
  import re
2
+ from collections.abc import Mapping, MutableMapping
2
3
  from enum import Enum
3
- from typing import Any, Mapping, MutableMapping, Optional, Union
4
+ from typing import Any, Optional, Union
4
5
 
5
6
  import requests
6
- from lxml import etree # nosec
7
7
 
8
8
  COLON_SPLIT_RE = re.compile(r"\s*,\s*")
9
9
 
@@ -82,6 +82,9 @@ class Connection:
82
82
  **kwargs: Any,
83
83
  ) -> Any:
84
84
  """Get the given URL (relative to the root of API)."""
85
+
86
+ from lxml import etree # nosec
87
+
85
88
  with self.session.get(
86
89
  self.base_url + url,
87
90
  headers=self._merge_headers(headers, cors),
@@ -1,4 +1,6 @@
1
+ import json
1
2
  import os
3
+ import subprocess # nosec
2
4
  from typing import TYPE_CHECKING, Any, Optional
3
5
 
4
6
  import numpy as np
@@ -42,6 +44,20 @@ def check_image_file(
42
44
  check_image(result_folder, result, expected_filename, level, generate_expected_image, use_mask)
43
45
 
44
46
 
47
+ def normalize_image(image: NpNdarrayInt) -> NpNdarrayInt:
48
+ """
49
+ Normalize the image to be comparable.
50
+
51
+ - Remove the alpha channel
52
+ - Convert to uint8
53
+ """
54
+ if len(image.shape) == 3 and image.shape[2] == 4:
55
+ image = skimage.color.rgba2rgb(image)
56
+ if np.issubdtype(image.dtype, np.floating):
57
+ image = (image * 255).astype("uint8")
58
+ return image
59
+
60
+
45
61
  def check_image(
46
62
  result_folder: str,
47
63
  image_to_check: NpNdarrayInt,
@@ -80,11 +96,30 @@ def check_image(
80
96
  result_filename = os.path.join(result_folder, f"{image_file_basename}.result.png")
81
97
  diff_filename = os.path.join(result_folder, f"{image_file_basename}.diff.png")
82
98
 
99
+ image_to_check = normalize_image(image_to_check)
100
+
83
101
  mask = None
84
102
  if mask_filename is not None:
85
103
  mask = skimage.io.imread(mask_filename)
104
+
86
105
  assert mask is not None, "Wrong mask: " + mask_filename
87
- image_to_check[mask == 0] = [255, 255, 255]
106
+
107
+ # Normalize the mask
108
+ if len(mask.shape) == 3 and mask.shape[2] == 3:
109
+ mask = skimage.color.rgb2gray(mask)
110
+
111
+ if len(mask.shape) == 3 and mask.shape[2] == 4:
112
+ mask = skimage.color.rgba2gray(mask)
113
+
114
+ if np.issubdtype(mask.dtype, np.floating):
115
+ mask = (mask * 255).astype("uint8")
116
+
117
+ assert ((0 < mask) & (mask < 255)).sum() == 0, "Mask should be only black and white image"
118
+
119
+ # Convert to boolean
120
+ mask = mask == 0
121
+
122
+ image_to_check[mask] = [255, 255, 255]
88
123
 
89
124
  if not os.path.exists(result_folder):
90
125
  os.makedirs(result_folder)
@@ -97,12 +132,13 @@ def check_image(
97
132
  assert False, "Expected image not found: " + expected_filename
98
133
  expected = skimage.io.imread(expected_filename)
99
134
  assert expected is not None, "Wrong image: " + expected_filename
135
+ expected = normalize_image(expected)
100
136
 
101
137
  if mask is not None:
102
- expected[mask == 0] = [255, 255, 255]
138
+ expected[mask] = [255, 255, 255]
103
139
 
104
140
  score, diff = skimage.metrics.structural_similarity(
105
- expected, image_to_check, multichannel=True, full=True
141
+ expected, image_to_check, multichannel=True, full=True, channel_axis=2
106
142
  )
107
143
  diff = (255 - diff * 255).astype("uint8")
108
144
 
@@ -115,3 +151,59 @@ def check_image(
115
151
  assert (
116
152
  score >= level
117
153
  ), f"{result_filename} != {expected_filename} => {diff_filename} ({score} < {level})"
154
+
155
+
156
+ def check_screenshot(
157
+ url: str,
158
+ result_folder: str,
159
+ expected_filename: str,
160
+ width: int = 800,
161
+ height: int = 600,
162
+ headers: Optional[dict[str, str]] = None,
163
+ media: Optional[list[dict[str, str]]] = None,
164
+ level: float = 1.0,
165
+ generate_expected_image: bool = False,
166
+ use_mask: bool = True,
167
+ ) -> None:
168
+ """
169
+ Test that the screenshot of the `url` corresponds to the image `expected_filename`.
170
+
171
+ Requires nodejs with puppeteer and commander to be installed.
172
+ """
173
+
174
+ if headers is None:
175
+ headers = {}
176
+ if media is None:
177
+ media = []
178
+
179
+ if not os.path.exists(os.path.join(os.path.dirname(__file__), "node_modules")):
180
+ subprocess.run(["npm", "install"], cwd=os.path.dirname(__file__), check=True) # nosec
181
+
182
+ image_file_basename = os.path.splitext(os.path.basename(expected_filename))[0]
183
+ if image_file_basename.endswith(".expected"):
184
+ image_file_basename = os.path.splitext(image_file_basename)[0]
185
+
186
+ result_folder = os.path.abspath(result_folder)
187
+ actual_filename = os.path.join(result_folder, f"{image_file_basename}.actual.png")
188
+ subprocess.run( # nosec
189
+ [
190
+ "node",
191
+ "screenshot.js",
192
+ f"--url={url}",
193
+ f"--width={width}",
194
+ f"--height={height}",
195
+ f"--headers={json.dumps(headers)}",
196
+ f"--media={json.dumps(media)}",
197
+ f"--output={actual_filename}",
198
+ ],
199
+ cwd=os.path.dirname(__file__),
200
+ check=True,
201
+ )
202
+ check_image(
203
+ result_folder,
204
+ skimage.io.imread(actual_filename)[:, :, :3],
205
+ expected_filename,
206
+ level,
207
+ generate_expected_image,
208
+ use_mask,
209
+ )