c2cwsgiutils 5.1.7.dev20230901073305__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 (69) hide show
  1. c2cwsgiutils/__init__.py +13 -13
  2. c2cwsgiutils/acceptance/connection.py +5 -2
  3. c2cwsgiutils/acceptance/image.py +98 -4
  4. c2cwsgiutils/acceptance/package-lock.json +1933 -0
  5. c2cwsgiutils/acceptance/package.json +7 -0
  6. c2cwsgiutils/acceptance/print.py +4 -4
  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 +8 -7
  14. c2cwsgiutils/client_info.py +5 -5
  15. c2cwsgiutils/config_utils.py +2 -1
  16. c2cwsgiutils/coverage_setup.py +2 -2
  17. c2cwsgiutils/db.py +58 -37
  18. c2cwsgiutils/db_maintenance_view.py +2 -1
  19. c2cwsgiutils/debug/_listeners.py +10 -9
  20. c2cwsgiutils/debug/_views.py +12 -11
  21. c2cwsgiutils/debug/utils.py +5 -5
  22. c2cwsgiutils/errors.py +7 -6
  23. c2cwsgiutils/health_check.py +96 -85
  24. c2cwsgiutils/index.py +90 -105
  25. c2cwsgiutils/loader.py +3 -3
  26. c2cwsgiutils/logging_view.py +3 -2
  27. c2cwsgiutils/models_graph.py +8 -6
  28. c2cwsgiutils/prometheus.py +175 -57
  29. c2cwsgiutils/pyramid.py +4 -2
  30. c2cwsgiutils/pyramid_logging.py +2 -1
  31. c2cwsgiutils/redis_stats.py +13 -11
  32. c2cwsgiutils/redis_utils.py +15 -14
  33. c2cwsgiutils/request_tracking/__init__.py +36 -30
  34. c2cwsgiutils/request_tracking/_sql.py +3 -1
  35. c2cwsgiutils/scripts/genversion.py +4 -4
  36. c2cwsgiutils/scripts/stats_db.py +130 -68
  37. c2cwsgiutils/scripts/test_print.py +1 -1
  38. c2cwsgiutils/sentry.py +2 -1
  39. c2cwsgiutils/setup_process.py +13 -17
  40. c2cwsgiutils/sql_profiler/_impl.py +12 -5
  41. c2cwsgiutils/sqlalchemylogger/README.md +48 -0
  42. c2cwsgiutils/sqlalchemylogger/_models.py +7 -4
  43. c2cwsgiutils/sqlalchemylogger/examples/example.py +15 -0
  44. c2cwsgiutils/sqlalchemylogger/handlers.py +11 -8
  45. c2cwsgiutils/static/favicon-16x16.png +0 -0
  46. c2cwsgiutils/static/favicon-32x32.png +0 -0
  47. c2cwsgiutils/stats_pyramid/__init__.py +7 -11
  48. c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
  49. c2cwsgiutils/stats_pyramid/_pyramid_spy.py +29 -20
  50. c2cwsgiutils/templates/index.html.mako +50 -0
  51. c2cwsgiutils/version.py +49 -16
  52. c2cwsgiutils-5.2.1.dev197.dist-info/LICENSE +22 -0
  53. {c2cwsgiutils-5.1.7.dev20230901073305.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +187 -135
  54. c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
  55. {c2cwsgiutils-5.1.7.dev20230901073305.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -2
  56. c2cwsgiutils-5.2.1.dev197.dist-info/entry_points.txt +21 -0
  57. c2cwsgiutils/acceptance/composition.py +0 -129
  58. c2cwsgiutils/metrics.py +0 -110
  59. c2cwsgiutils/scripts/check_es.py +0 -130
  60. c2cwsgiutils/scripts/coverage_report.py +0 -36
  61. c2cwsgiutils/stats.py +0 -355
  62. c2cwsgiutils/stats_pyramid/_views.py +0 -16
  63. c2cwsgiutils-5.1.7.dev20230901073305.data/scripts/c2cwsgiutils-run +0 -32
  64. c2cwsgiutils-5.1.7.dev20230901073305.dist-info/LICENSE.txt +0 -28
  65. c2cwsgiutils-5.1.7.dev20230901073305.dist-info/RECORD +0 -69
  66. c2cwsgiutils-5.1.7.dev20230901073305.dist-info/entry_points.txt +0 -25
  67. c2cwsgiutils-5.1.7.dev20230901073305.dist-info/top_level.txt +0 -2
  68. tests/acceptance/__init__.py +0 -0
  69. tests/acceptance/test_utils.py +0 -13
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,15 +29,15 @@ 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}"]
38
38
  stream_match = stream_re.match(block["args"])
39
39
  if stream_match is None:
40
- raise Exception(f"Could not parse args of handler {hh}")
40
+ raise Exception(f"Could not parse args of handler {hh}") # pylint: disable=broad-exception-raised
41
41
  args = stream_match.groups()[0]
42
42
  c = block["class"]
43
43
  if "." not in c:
@@ -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
@@ -8,7 +10,9 @@ import skimage.metrics
8
10
  import skimage.transform
9
11
 
10
12
  if TYPE_CHECKING:
11
- NpNdarrayInt = np.ndarray[np.uint8, Any]
13
+ from typing import TypeAlias
14
+
15
+ NpNdarrayInt: TypeAlias = np.ndarray[np.uint8, Any]
12
16
  else:
13
17
  NpNdarrayInt = np.ndarray
14
18
 
@@ -40,6 +44,20 @@ def check_image_file(
40
44
  check_image(result_folder, result, expected_filename, level, generate_expected_image, use_mask)
41
45
 
42
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
+
43
61
  def check_image(
44
62
  result_folder: str,
45
63
  image_to_check: NpNdarrayInt,
@@ -78,11 +96,30 @@ def check_image(
78
96
  result_filename = os.path.join(result_folder, f"{image_file_basename}.result.png")
79
97
  diff_filename = os.path.join(result_folder, f"{image_file_basename}.diff.png")
80
98
 
99
+ image_to_check = normalize_image(image_to_check)
100
+
81
101
  mask = None
82
102
  if mask_filename is not None:
83
103
  mask = skimage.io.imread(mask_filename)
104
+
84
105
  assert mask is not None, "Wrong mask: " + mask_filename
85
- 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]
86
123
 
87
124
  if not os.path.exists(result_folder):
88
125
  os.makedirs(result_folder)
@@ -95,12 +132,13 @@ def check_image(
95
132
  assert False, "Expected image not found: " + expected_filename
96
133
  expected = skimage.io.imread(expected_filename)
97
134
  assert expected is not None, "Wrong image: " + expected_filename
135
+ expected = normalize_image(expected)
98
136
 
99
137
  if mask is not None:
100
- expected[mask == 0] = [255, 255, 255]
138
+ expected[mask] = [255, 255, 255]
101
139
 
102
140
  score, diff = skimage.metrics.structural_similarity(
103
- expected, image_to_check, multichannel=True, full=True
141
+ expected, image_to_check, multichannel=True, full=True, channel_axis=2
104
142
  )
105
143
  diff = (255 - diff * 255).astype("uint8")
106
144
 
@@ -113,3 +151,59 @@ def check_image(
113
151
  assert (
114
152
  score >= level
115
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
+ )