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.
- c2cwsgiutils/__init__.py +12 -12
- c2cwsgiutils/acceptance/connection.py +5 -2
- c2cwsgiutils/acceptance/image.py +95 -3
- c2cwsgiutils/acceptance/package-lock.json +1933 -0
- c2cwsgiutils/acceptance/package.json +7 -0
- c2cwsgiutils/acceptance/print.py +3 -3
- c2cwsgiutils/acceptance/screenshot.js +62 -0
- c2cwsgiutils/acceptance/utils.py +14 -22
- c2cwsgiutils/auth.py +4 -4
- c2cwsgiutils/broadcast/__init__.py +15 -7
- c2cwsgiutils/broadcast/interface.py +3 -2
- c2cwsgiutils/broadcast/local.py +3 -2
- c2cwsgiutils/broadcast/redis.py +6 -5
- c2cwsgiutils/client_info.py +5 -5
- c2cwsgiutils/config_utils.py +2 -1
- c2cwsgiutils/db.py +20 -11
- c2cwsgiutils/db_maintenance_view.py +2 -1
- c2cwsgiutils/debug/_listeners.py +7 -6
- c2cwsgiutils/debug/_views.py +11 -10
- c2cwsgiutils/debug/utils.py +5 -5
- c2cwsgiutils/health_check.py +72 -73
- c2cwsgiutils/index.py +90 -105
- c2cwsgiutils/loader.py +3 -3
- c2cwsgiutils/logging_view.py +3 -2
- c2cwsgiutils/models_graph.py +4 -4
- c2cwsgiutils/prometheus.py +175 -57
- c2cwsgiutils/pyramid.py +4 -2
- c2cwsgiutils/pyramid_logging.py +2 -1
- c2cwsgiutils/redis_stats.py +13 -11
- c2cwsgiutils/redis_utils.py +11 -5
- c2cwsgiutils/request_tracking/__init__.py +36 -30
- c2cwsgiutils/scripts/genversion.py +4 -4
- c2cwsgiutils/scripts/stats_db.py +92 -60
- c2cwsgiutils/sentry.py +2 -1
- c2cwsgiutils/setup_process.py +12 -16
- c2cwsgiutils/sql_profiler/_impl.py +3 -2
- c2cwsgiutils/sqlalchemylogger/_models.py +2 -2
- c2cwsgiutils/sqlalchemylogger/handlers.py +6 -6
- c2cwsgiutils/static/favicon-16x16.png +0 -0
- c2cwsgiutils/static/favicon-32x32.png +0 -0
- c2cwsgiutils/stats_pyramid/__init__.py +7 -11
- c2cwsgiutils/stats_pyramid/_db_spy.py +14 -11
- c2cwsgiutils/stats_pyramid/_pyramid_spy.py +27 -21
- c2cwsgiutils/templates/index.html.mako +50 -0
- c2cwsgiutils/version.py +49 -16
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/METADATA +168 -99
- c2cwsgiutils-5.2.1.dev197.dist-info/RECORD +67 -0
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/WHEEL +1 -1
- c2cwsgiutils/acceptance/composition.py +0 -129
- c2cwsgiutils/metrics.py +0 -110
- c2cwsgiutils/scripts/check_es.py +0 -130
- c2cwsgiutils/stats.py +0 -344
- c2cwsgiutils/stats_pyramid/_views.py +0 -16
- c2cwsgiutils-5.2.1.dist-info/RECORD +0 -66
- {c2cwsgiutils-5.2.1.dist-info → c2cwsgiutils-5.2.1.dev197.dist-info}/LICENSE +0 -0
- {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
|
7
|
+
from typing import Any
|
8
8
|
|
9
9
|
LOG = logging.getLogger(__name__)
|
10
10
|
|
11
11
|
|
12
|
-
def get_config_defaults() ->
|
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:
|
22
|
-
lowercase_keys:
|
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) ->
|
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:
|
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) ->
|
57
|
-
out:
|
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) ->
|
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:
|
80
|
-
root:
|
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:
|
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,
|
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),
|
c2cwsgiutils/acceptance/image.py
CHANGED
@@ -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
|
-
|
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
|
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
|
+
)
|