c2cwsgiutils 6.2.0.dev74__tar.gz → 6.2.0.dev76__tar.gz
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-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/PKG-INFO +1 -1
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/__init__.py +4 -4
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/acceptance/__init__.py +4 -1
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/acceptance/connection.py +45 -32
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/acceptance/image.py +45 -39
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/acceptance/print.py +2 -2
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/acceptance/utils.py +4 -4
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/auth.py +20 -13
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/broadcast/__init__.py +25 -16
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/broadcast/interface.py +8 -4
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/broadcast/local.py +9 -4
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/broadcast/redis.py +19 -12
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/client_info.py +3 -2
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/config_utils.py +14 -10
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/coverage_setup.py +5 -5
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/db.py +54 -47
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/db_maintenance_view.py +7 -8
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/debug/__init__.py +1 -2
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/debug/_listeners.py +6 -4
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/debug/_views.py +15 -10
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/debug/utils.py +5 -5
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/errors.py +15 -10
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/health_check.py +79 -60
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/index.py +35 -37
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/loader.py +5 -4
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/logging_view.py +13 -6
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/models_graph.py +5 -5
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/pretty_json.py +3 -2
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/profiler.py +1 -2
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/prometheus.py +6 -6
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/pyramid.py +1 -1
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/pyramid_logging.py +9 -4
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/redis_stats.py +12 -7
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/redis_utils.py +18 -10
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/request_tracking/__init__.py +20 -12
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/request_tracking/_sql.py +2 -1
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/scripts/genversion.py +10 -9
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/scripts/stats_db.py +29 -13
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sentry.py +44 -20
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/setup_process.py +7 -4
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sql_profiler/_impl.py +12 -11
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sqlalchemylogger/_models.py +3 -3
- c2cwsgiutils-6.2.0.dev76/c2cwsgiutils/sqlalchemylogger/examples/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sqlalchemylogger/handlers.py +8 -9
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/stats_pyramid/_db_spy.py +12 -3
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/stats_pyramid/_pyramid_spy.py +10 -9
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/version.py +10 -7
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/pyproject.toml +34 -37
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/LICENSE +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/README.md +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/acceptance/package-lock.json +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/acceptance/package.json +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/acceptance/screenshot.js +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/broadcast/utils.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/py.typed +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/scripts/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/scripts/test_print.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/services.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sql_profiler/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sqlalchemylogger/README.md +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sqlalchemylogger/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sqlalchemylogger/_filters.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/sqlalchemylogger/examples/example.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/static/favicon-16x16.png +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/static/favicon-32x32.png +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/stats_pyramid/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev74 → c2cwsgiutils-6.2.0.dev76}/c2cwsgiutils/templates/index.html.mako +0 -0
@@ -35,7 +35,8 @@ def _create_handlers(config: configparser.ConfigParser) -> dict[str, Any]:
|
|
35
35
|
for hh in handlers:
|
36
36
|
block = config[f"handler_{hh}"]
|
37
37
|
if "args" in block:
|
38
|
-
|
38
|
+
message = f"Can not parse args of handlers {hh}, use kwargs instead."
|
39
|
+
raise ValueError(message)
|
39
40
|
c = block["class"]
|
40
41
|
if "." not in c:
|
41
42
|
# classes like StreamHandler does not need the prefix in the ini so we add it here
|
@@ -116,10 +117,9 @@ def get_paste_config() -> str:
|
|
116
117
|
for val in sys.argv:
|
117
118
|
if next_one:
|
118
119
|
return val
|
119
|
-
if val.startswith("--paste="
|
120
|
+
if val.startswith(("--paste=", "--paster=")):
|
120
121
|
return val.split("=")[1]
|
121
122
|
if val in ["--paste", "--paster"]:
|
122
123
|
next_one = True
|
123
124
|
|
124
|
-
|
125
|
-
return fallback
|
125
|
+
return os.environ.get("C2CWSGIUTILS_CONFIG", "production.ini")
|
@@ -7,7 +7,10 @@ _LOG = logging.getLogger(__name__)
|
|
7
7
|
|
8
8
|
|
9
9
|
def retry(
|
10
|
-
exception_to_check: typing.Any,
|
10
|
+
exception_to_check: typing.Any,
|
11
|
+
tries: float = 3,
|
12
|
+
delay: float = 0.5,
|
13
|
+
backoff: float = 2,
|
11
14
|
) -> typing.Callable[..., typing.Any]:
|
12
15
|
"""
|
13
16
|
Retry calling the decorated function using an exponential backoff.
|
@@ -1,7 +1,8 @@
|
|
1
1
|
import re
|
2
2
|
from collections.abc import Mapping, MutableMapping
|
3
3
|
from enum import Enum
|
4
|
-
from
|
4
|
+
from pathlib import Path
|
5
|
+
from typing import Any
|
5
6
|
|
6
7
|
import requests
|
7
8
|
|
@@ -32,10 +33,10 @@ class Connection:
|
|
32
33
|
url: str,
|
33
34
|
expected_status: int = 200,
|
34
35
|
cors: bool = True,
|
35
|
-
headers:
|
36
|
+
headers: Mapping[str, str] | None = None,
|
36
37
|
cache_expected: CacheExpected = CacheExpected.NO,
|
37
38
|
**kwargs: Any,
|
38
|
-
) ->
|
39
|
+
) -> str | None:
|
39
40
|
"""Get the given URL (relative to the root of API)."""
|
40
41
|
with self.session.get(self.base_url + url, headers=self._merge_headers(headers, cors), **kwargs) as r:
|
41
42
|
check_response(r, expected_status, cache_expected=cache_expected)
|
@@ -46,7 +47,7 @@ class Connection:
|
|
46
47
|
self,
|
47
48
|
url: str,
|
48
49
|
expected_status: int = 200,
|
49
|
-
headers:
|
50
|
+
headers: Mapping[str, str] | None = None,
|
50
51
|
cors: bool = True,
|
51
52
|
cache_expected: CacheExpected = CacheExpected.NO,
|
52
53
|
**kwargs: Any,
|
@@ -61,7 +62,7 @@ class Connection:
|
|
61
62
|
self,
|
62
63
|
url: str,
|
63
64
|
expected_status: int = 200,
|
64
|
-
headers:
|
65
|
+
headers: Mapping[str, str] | None = None,
|
65
66
|
cors: bool = True,
|
66
67
|
cache_expected: CacheExpected = CacheExpected.NO,
|
67
68
|
**kwargs: Any,
|
@@ -75,9 +76,9 @@ class Connection:
|
|
75
76
|
def get_xml(
|
76
77
|
self,
|
77
78
|
url: str,
|
78
|
-
schema:
|
79
|
+
schema: Path | None = None,
|
79
80
|
expected_status: int = 200,
|
80
|
-
headers:
|
81
|
+
headers: Mapping[str, str] | None = None,
|
81
82
|
cors: bool = True,
|
82
83
|
cache_expected: CacheExpected = CacheExpected.NO,
|
83
84
|
**kwargs: Any,
|
@@ -96,7 +97,7 @@ class Connection:
|
|
96
97
|
r.raw.decode_content = True
|
97
98
|
doc = etree.parse(r.raw) # noqa: S320
|
98
99
|
if schema is not None:
|
99
|
-
with open(
|
100
|
+
with schema.open(encoding="utf-8") as schema_file:
|
100
101
|
xml_schema = etree.XMLSchema(etree.parse(schema_file)) # noqa: S320
|
101
102
|
xml_schema.assertValid(doc)
|
102
103
|
return doc
|
@@ -105,14 +106,16 @@ class Connection:
|
|
105
106
|
self,
|
106
107
|
url: str,
|
107
108
|
expected_status: int = 200,
|
108
|
-
headers:
|
109
|
+
headers: Mapping[str, str] | None = None,
|
109
110
|
cors: bool = True,
|
110
111
|
cache_expected: CacheExpected = CacheExpected.NO,
|
111
112
|
**kwargs: Any,
|
112
113
|
) -> Any:
|
113
114
|
"""POST the given URL (relative to the root of API)."""
|
114
115
|
with self.session.post(
|
115
|
-
self.base_url + url,
|
116
|
+
self.base_url + url,
|
117
|
+
headers=self._merge_headers(headers, cors),
|
118
|
+
**kwargs,
|
116
119
|
) as r:
|
117
120
|
check_response(r, expected_status, cache_expected=cache_expected)
|
118
121
|
self._check_cors(cors, r)
|
@@ -122,14 +125,16 @@ class Connection:
|
|
122
125
|
self,
|
123
126
|
url: str,
|
124
127
|
expected_status: int = 200,
|
125
|
-
headers:
|
128
|
+
headers: Mapping[str, str] | None = None,
|
126
129
|
cors: bool = True,
|
127
130
|
cache_expected: CacheExpected = CacheExpected.NO,
|
128
131
|
**kwargs: Any,
|
129
132
|
) -> Any:
|
130
133
|
"""POST files to the the given URL (relative to the root of API)."""
|
131
134
|
with self.session.post(
|
132
|
-
self.base_url + url,
|
135
|
+
self.base_url + url,
|
136
|
+
headers=self._merge_headers(headers, cors),
|
137
|
+
**kwargs,
|
133
138
|
) as r:
|
134
139
|
check_response(r, expected_status, cache_expected)
|
135
140
|
self._check_cors(cors, r)
|
@@ -139,14 +144,16 @@ class Connection:
|
|
139
144
|
self,
|
140
145
|
url: str,
|
141
146
|
expected_status: int = 200,
|
142
|
-
headers:
|
147
|
+
headers: Mapping[str, str] | None = None,
|
143
148
|
cors: bool = True,
|
144
149
|
cache_expected: CacheExpected = CacheExpected.NO,
|
145
150
|
**kwargs: Any,
|
146
|
-
) ->
|
151
|
+
) -> str | None:
|
147
152
|
"""POST the given URL (relative to the root of API)."""
|
148
153
|
with self.session.post(
|
149
|
-
self.base_url + url,
|
154
|
+
self.base_url + url,
|
155
|
+
headers=self._merge_headers(headers, cors),
|
156
|
+
**kwargs,
|
150
157
|
) as r:
|
151
158
|
check_response(r, expected_status, cache_expected)
|
152
159
|
self._check_cors(cors, r)
|
@@ -156,7 +163,7 @@ class Connection:
|
|
156
163
|
self,
|
157
164
|
url: str,
|
158
165
|
expected_status: int = 200,
|
159
|
-
headers:
|
166
|
+
headers: Mapping[str, str] | None = None,
|
160
167
|
cors: bool = True,
|
161
168
|
cache_expected: CacheExpected = CacheExpected.NO,
|
162
169
|
**kwargs: Any,
|
@@ -171,14 +178,16 @@ class Connection:
|
|
171
178
|
self,
|
172
179
|
url: str,
|
173
180
|
expected_status: int = 200,
|
174
|
-
headers:
|
181
|
+
headers: Mapping[str, str] | None = None,
|
175
182
|
cors: bool = True,
|
176
183
|
cache_expected: CacheExpected = CacheExpected.NO,
|
177
184
|
**kwargs: Any,
|
178
185
|
) -> Any:
|
179
186
|
"""PATCH the given URL (relative to the root of API)."""
|
180
187
|
with self.session.patch(
|
181
|
-
self.base_url + url,
|
188
|
+
self.base_url + url,
|
189
|
+
headers=self._merge_headers(headers, cors),
|
190
|
+
**kwargs,
|
182
191
|
) as r:
|
183
192
|
check_response(r, expected_status, cache_expected)
|
184
193
|
self._check_cors(cors, r)
|
@@ -188,14 +197,16 @@ class Connection:
|
|
188
197
|
self,
|
189
198
|
url: str,
|
190
199
|
expected_status: int = 204,
|
191
|
-
headers:
|
200
|
+
headers: Mapping[str, str] | None = None,
|
192
201
|
cors: bool = True,
|
193
202
|
cache_expected: CacheExpected = CacheExpected.NO,
|
194
203
|
**kwargs: Any,
|
195
204
|
) -> requests.Response:
|
196
205
|
"""DELETE the given URL (relative to the root of API)."""
|
197
206
|
with self.session.delete(
|
198
|
-
self.base_url + url,
|
207
|
+
self.base_url + url,
|
208
|
+
headers=self._merge_headers(headers, cors),
|
209
|
+
**kwargs,
|
199
210
|
) as r:
|
200
211
|
check_response(r, expected_status, cache_expected)
|
201
212
|
self._check_cors(cors, r)
|
@@ -205,13 +216,15 @@ class Connection:
|
|
205
216
|
self,
|
206
217
|
url: str,
|
207
218
|
expected_status: int = 200,
|
208
|
-
headers:
|
219
|
+
headers: Mapping[str, str] | None = None,
|
209
220
|
cache_expected: CacheExpected = CacheExpected.NO,
|
210
221
|
**kwargs: Any,
|
211
222
|
) -> requests.Response:
|
212
223
|
"""Get the given URL (relative to the root of API)."""
|
213
224
|
with self.session.options(
|
214
|
-
self.base_url + url,
|
225
|
+
self.base_url + url,
|
226
|
+
headers=self._merge_headers(headers, cors=False),
|
227
|
+
**kwargs,
|
215
228
|
) as r:
|
216
229
|
check_response(r, expected_status, cache_expected=cache_expected)
|
217
230
|
return r
|
@@ -219,8 +232,7 @@ class Connection:
|
|
219
232
|
def _cors_headers(self, cors: bool) -> Mapping[str, str]:
|
220
233
|
if cors:
|
221
234
|
return {"Origin": self.origin}
|
222
|
-
|
223
|
-
return {}
|
235
|
+
return {}
|
224
236
|
|
225
237
|
def _check_cors(self, cors: bool, r: requests.Response) -> None:
|
226
238
|
if cors:
|
@@ -230,8 +242,10 @@ class Connection:
|
|
230
242
|
assert r.headers["Access-Control-Allow-Origin"] == "*"
|
231
243
|
|
232
244
|
def _merge_headers(
|
233
|
-
self,
|
234
|
-
|
245
|
+
self,
|
246
|
+
headers: Mapping[str, str | bytes] | None,
|
247
|
+
cors: bool,
|
248
|
+
) -> MutableMapping[str, str | bytes]:
|
235
249
|
merged = dict(headers) if headers is not None else {}
|
236
250
|
if self.session.headers is not None:
|
237
251
|
merged.update(self.session.headers)
|
@@ -265,9 +279,8 @@ def check_response(
|
|
265
279
|
def _get_json(r: requests.Response) -> Any:
|
266
280
|
if r.status_code == 204:
|
267
281
|
return None
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
return r.json()
|
282
|
+
content_type = r.headers["Content-Type"].split(";")[0]
|
283
|
+
assert content_type == "application/json" or content_type.endswith("+json"), (
|
284
|
+
f"{r.status_code}, {content_type}, {r.text}"
|
285
|
+
)
|
286
|
+
return r.json()
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import json
|
2
|
-
import os
|
3
2
|
import subprocess # nosec
|
4
|
-
from
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import TYPE_CHECKING, Any
|
5
5
|
|
6
6
|
import numpy as np # pylint: disable=import-error
|
7
7
|
import skimage.color # pylint: disable=import-error
|
@@ -10,7 +10,7 @@ import skimage.metrics # pylint: disable=import-error
|
|
10
10
|
import skimage.transform # pylint: disable=import-error
|
11
11
|
|
12
12
|
if TYPE_CHECKING:
|
13
|
-
from
|
13
|
+
from typing import TypeAlias
|
14
14
|
|
15
15
|
NpNdarrayInt: TypeAlias = np.ndarray[np.uint8, Any]
|
16
16
|
else:
|
@@ -18,9 +18,9 @@ else:
|
|
18
18
|
|
19
19
|
|
20
20
|
def check_image_file(
|
21
|
-
result_folder: str,
|
22
|
-
image_filename_to_check: str,
|
23
|
-
expected_filename: str,
|
21
|
+
result_folder: str | Path,
|
22
|
+
image_filename_to_check: str | Path,
|
23
|
+
expected_filename: str | Path,
|
24
24
|
level: float = 1.0,
|
25
25
|
generate_expected_image: bool = False,
|
26
26
|
use_mask: bool = True,
|
@@ -40,7 +40,7 @@ def check_image_file(
|
|
40
40
|
we use it as a mask.
|
41
41
|
"""
|
42
42
|
result = skimage.io.imread(image_filename_to_check)
|
43
|
-
assert result is not None, "Wrong image: "
|
43
|
+
assert result is not None, f"Wrong image: {image_filename_to_check}"
|
44
44
|
check_image(result_folder, result, expected_filename, level, generate_expected_image, use_mask)
|
45
45
|
|
46
46
|
|
@@ -59,9 +59,9 @@ def normalize_image(image: NpNdarrayInt) -> NpNdarrayInt:
|
|
59
59
|
|
60
60
|
|
61
61
|
def check_image( # pylint: disable=too-many-locals,too-many-statements
|
62
|
-
result_folder: str,
|
62
|
+
result_folder: str | Path,
|
63
63
|
image_to_check: NpNdarrayInt,
|
64
|
-
expected_filename: str,
|
64
|
+
expected_filename: str | Path,
|
65
65
|
level: float = 1.0,
|
66
66
|
generate_expected_image: bool = False,
|
67
67
|
use_mask: bool = True,
|
@@ -92,18 +92,18 @@ def check_image( # pylint: disable=too-many-locals,too-many-statements
|
|
92
92
|
|
93
93
|
"""
|
94
94
|
assert image_to_check is not None, "Image required"
|
95
|
-
|
96
|
-
|
95
|
+
result_folder = Path(result_folder)
|
96
|
+
expected_filename = Path(expected_filename)
|
97
|
+
image_file_basename = expected_filename.stem
|
98
|
+
mask_filename: Path | None = None
|
97
99
|
if image_file_basename.endswith(".expected"):
|
98
|
-
image_file_basename =
|
100
|
+
image_file_basename = Path(image_file_basename).stem
|
99
101
|
if use_mask:
|
100
|
-
mask_filename =
|
101
|
-
|
102
|
-
)
|
103
|
-
if not os.path.isfile(mask_filename):
|
102
|
+
mask_filename = expected_filename.with_name(image_file_basename + ".mask.png")
|
103
|
+
if not mask_filename.is_file():
|
104
104
|
mask_filename = None
|
105
|
-
result_filename =
|
106
|
-
diff_filename =
|
105
|
+
result_filename = result_folder / f"{image_file_basename}.actual-masked.png"
|
106
|
+
diff_filename = result_folder / f"{image_file_basename}.diff.png"
|
107
107
|
|
108
108
|
image_to_check = normalize_image(image_to_check)
|
109
109
|
|
@@ -112,13 +112,15 @@ def check_image( # pylint: disable=too-many-locals,too-many-statements
|
|
112
112
|
background_color = [255, 255, 255]
|
113
113
|
for color in range(3):
|
114
114
|
img_hist, _ = skimage.exposure.histogram(
|
115
|
-
image_to_check[..., color],
|
115
|
+
image_to_check[..., color],
|
116
|
+
nbins=256,
|
117
|
+
source_range="dtype",
|
116
118
|
)
|
117
119
|
background_color[color] = np.argmax(img_hist)
|
118
120
|
|
119
121
|
mask = skimage.io.imread(mask_filename)
|
120
122
|
|
121
|
-
assert mask is not None, "Wrong mask: " + mask_filename
|
123
|
+
assert mask is not None, "Wrong mask: " + str(mask_filename)
|
122
124
|
|
123
125
|
# Normalize the mask
|
124
126
|
if len(mask.shape) == 3 and mask.shape[2] == 3:
|
@@ -135,25 +137,25 @@ def check_image( # pylint: disable=too-many-locals,too-many-statements
|
|
135
137
|
# Convert to boolean
|
136
138
|
mask = mask == 0
|
137
139
|
|
138
|
-
assert mask.shape[0] == image_to_check.shape[0] and mask.shape[1] == image_to_check.shape[1], (
|
140
|
+
assert mask.shape[0] == image_to_check.shape[0] and mask.shape[1] == image_to_check.shape[1], ( # noqa: PT018
|
139
141
|
f"Mask and image should have the same shape ({mask.shape} != {image_to_check.shape})"
|
140
142
|
)
|
141
143
|
image_to_check[mask] = background_color
|
142
144
|
|
143
|
-
if not
|
144
|
-
|
145
|
+
if not result_folder.exists():
|
146
|
+
result_folder.mkdir(parents=True)
|
145
147
|
if generate_expected_image:
|
146
148
|
skimage.io.imsave(expected_filename, image_to_check)
|
147
149
|
return
|
148
|
-
if not
|
150
|
+
if not expected_filename.is_file():
|
149
151
|
skimage.io.imsave(expected_filename, image_to_check)
|
150
|
-
raise AssertionError("Expected image not found: " + expected_filename)
|
152
|
+
raise AssertionError("Expected image not found: " + str(expected_filename))
|
151
153
|
expected = skimage.io.imread(expected_filename)
|
152
|
-
assert expected is not None, "Wrong image: " + expected_filename
|
154
|
+
assert expected is not None, "Wrong image: " + str(expected_filename)
|
153
155
|
expected = normalize_image(expected)
|
154
156
|
|
155
157
|
if mask is not None:
|
156
|
-
assert expected.shape[0] == mask.shape[0] and expected.shape[1] == mask.shape[1], (
|
158
|
+
assert expected.shape[0] == mask.shape[0] and expected.shape[1] == mask.shape[1], ( # noqa: PT018
|
157
159
|
f"Mask and expected image should have the same shape ({mask.shape} != {expected.shape})"
|
158
160
|
)
|
159
161
|
expected[mask] = background_color
|
@@ -162,7 +164,11 @@ def check_image( # pylint: disable=too-many-locals,too-many-statements
|
|
162
164
|
f"Images have different shapes expected {expected.shape} != actual {image_to_check.shape}"
|
163
165
|
)
|
164
166
|
score, diff = skimage.metrics.structural_similarity(
|
165
|
-
expected,
|
167
|
+
expected,
|
168
|
+
image_to_check,
|
169
|
+
multichannel=True,
|
170
|
+
full=True,
|
171
|
+
channel_axis=2,
|
166
172
|
)
|
167
173
|
diff = (255 - diff * 255).astype("uint8")
|
168
174
|
|
@@ -179,13 +185,13 @@ def check_image( # pylint: disable=too-many-locals,too-many-statements
|
|
179
185
|
|
180
186
|
def check_screenshot(
|
181
187
|
url: str,
|
182
|
-
result_folder: str,
|
183
|
-
expected_filename: str,
|
188
|
+
result_folder: str | Path,
|
189
|
+
expected_filename: str | Path,
|
184
190
|
width: int = 800,
|
185
191
|
height: int = 600,
|
186
192
|
sleep: int = 100,
|
187
|
-
headers:
|
188
|
-
media:
|
193
|
+
headers: dict[str, str] | None = None,
|
194
|
+
media: list[dict[str, str]] | None = None,
|
189
195
|
level: float = 1.0,
|
190
196
|
generate_expected_image: bool = False,
|
191
197
|
use_mask: bool = True,
|
@@ -216,15 +222,15 @@ def check_screenshot(
|
|
216
222
|
if media is None:
|
217
223
|
media = []
|
218
224
|
|
219
|
-
if not
|
220
|
-
subprocess.run(["npm", "install"], cwd=
|
225
|
+
if not Path(__file__).parent.joinpath("node_modules").exists():
|
226
|
+
subprocess.run(["npm", "install"], cwd=Path(__file__).parent, check=True) # nosec
|
221
227
|
|
222
|
-
image_file_basename =
|
228
|
+
image_file_basename = Path(expected_filename).stem
|
223
229
|
if image_file_basename.endswith(".expected"):
|
224
|
-
image_file_basename =
|
230
|
+
image_file_basename = Path(image_file_basename).stem
|
225
231
|
|
226
|
-
result_folder =
|
227
|
-
actual_filename =
|
232
|
+
result_folder = Path(result_folder).resolve()
|
233
|
+
actual_filename = result_folder / f"{image_file_basename}.actual.png"
|
228
234
|
subprocess.run( # nosec
|
229
235
|
[
|
230
236
|
"node",
|
@@ -237,7 +243,7 @@ def check_screenshot(
|
|
237
243
|
f"--media={json.dumps(media)}",
|
238
244
|
f"--output={actual_filename}",
|
239
245
|
],
|
240
|
-
cwd=
|
246
|
+
cwd=Path(__file__).parent,
|
241
247
|
check=True,
|
242
248
|
)
|
243
249
|
check_image(
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import functools
|
2
2
|
import json
|
3
3
|
import logging
|
4
|
-
from typing import Any
|
4
|
+
from typing import Any
|
5
5
|
|
6
6
|
import requests
|
7
7
|
|
@@ -56,7 +56,7 @@ class PrintConnection(connection.Connection):
|
|
56
56
|
assert report.headers["Content-Type"] == "application/pdf"
|
57
57
|
return report
|
58
58
|
|
59
|
-
def _check_completion(self, ref: str) ->
|
59
|
+
def _check_completion(self, ref: str) -> Any | None:
|
60
60
|
status = self.get_json(f"status/{ref}.json")
|
61
61
|
if status["done"]:
|
62
62
|
return status
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import logging
|
2
2
|
import time
|
3
|
-
from
|
3
|
+
from collections.abc import Callable
|
4
|
+
from typing import Any
|
4
5
|
|
5
6
|
import pytest
|
6
7
|
import requests
|
@@ -18,8 +19,7 @@ def wait_url(url: str, timeout: float = _DEFAULT_TIMEOUT) -> None:
|
|
18
19
|
if r.status_code == 200:
|
19
20
|
_LOG.info("%s service started", url)
|
20
21
|
return True
|
21
|
-
|
22
|
-
return False
|
22
|
+
return False
|
23
23
|
|
24
24
|
retry_timeout(what, timeout=timeout)
|
25
25
|
|
@@ -43,7 +43,7 @@ def retry_timeout(what: Callable[[], Any], timeout: float = _DEFAULT_TIMEOUT, in
|
|
43
43
|
return ret
|
44
44
|
except NameError:
|
45
45
|
raise
|
46
|
-
except Exception as e: # pylint: disable=broad-
|
46
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
47
47
|
error = str(e)
|
48
48
|
_LOG.info(" Failed: %s", e)
|
49
49
|
if time.perf_counter() > timeout:
|
@@ -2,7 +2,7 @@ import hashlib
|
|
2
2
|
import logging
|
3
3
|
from collections.abc import Mapping
|
4
4
|
from enum import Enum
|
5
|
-
from typing import Any,
|
5
|
+
from typing import Any, TypedDict, cast
|
6
6
|
|
7
7
|
import jwt
|
8
8
|
import pyramid.request
|
@@ -51,15 +51,15 @@ class AuthConfig(TypedDict, total=False):
|
|
51
51
|
"""Configuration of the authentication."""
|
52
52
|
|
53
53
|
# The repository to check access to (<organization>/<repository>).
|
54
|
-
github_repository:
|
54
|
+
github_repository: str | None
|
55
55
|
# The type of access to check (admin|push|pull).
|
56
|
-
github_access_type:
|
56
|
+
github_access_type: str | None
|
57
57
|
|
58
58
|
|
59
59
|
def get_expected_secret(request: pyramid.request.Request) -> str:
|
60
60
|
"""Return the secret expected from the client."""
|
61
61
|
settings = request.registry.settings
|
62
|
-
return cast(str, env_or_settings(settings, SECRET_ENV, SECRET_PROP, False))
|
62
|
+
return cast(str, env_or_settings(settings, SECRET_ENV, SECRET_PROP, default=False))
|
63
63
|
|
64
64
|
|
65
65
|
def _hash_secret(secret: str) -> str:
|
@@ -80,10 +80,7 @@ def _is_auth_secret(request: pyramid.request.Request) -> bool:
|
|
80
80
|
secret = request.params.get("secret")
|
81
81
|
if secret is None:
|
82
82
|
secret = request.headers.get("X-API-Key")
|
83
|
-
if secret is None
|
84
|
-
secret_hash = request.cookies.get(SECRET_ENV)
|
85
|
-
else:
|
86
|
-
secret_hash = _hash_secret(secret)
|
83
|
+
secret_hash = request.cookies.get(SECRET_ENV) if secret is None else _hash_secret(secret)
|
87
84
|
|
88
85
|
if secret_hash is not None:
|
89
86
|
if secret_hash == "" or secret == "": # nosec
|
@@ -94,7 +91,12 @@ def _is_auth_secret(request: pyramid.request.Request) -> bool:
|
|
94
91
|
return False
|
95
92
|
# Login or refresh the cookie
|
96
93
|
request.response.set_cookie(
|
97
|
-
SECRET_ENV,
|
94
|
+
SECRET_ENV,
|
95
|
+
secret_hash,
|
96
|
+
max_age=_COOKIE_AGE,
|
97
|
+
httponly=True,
|
98
|
+
secure=True,
|
99
|
+
samesite="Strict",
|
98
100
|
)
|
99
101
|
# Since this could be used from outside c2cwsgiutils views, we cannot set the path to c2c
|
100
102
|
return True
|
@@ -158,7 +160,8 @@ def is_auth(request: pyramid.request.Request) -> bool:
|
|
158
160
|
def auth_view(request: pyramid.request.Request) -> None:
|
159
161
|
"""Get the authentication view."""
|
160
162
|
if not is_auth(request):
|
161
|
-
|
163
|
+
message = "Missing or invalid secret (parameter, X-API-Key header or cookie)"
|
164
|
+
raise HTTPForbidden(message)
|
162
165
|
|
163
166
|
|
164
167
|
class AuthenticationType(Enum):
|
@@ -172,7 +175,7 @@ class AuthenticationType(Enum):
|
|
172
175
|
GITHUB = 2
|
173
176
|
|
174
177
|
|
175
|
-
def auth_type(settings:
|
178
|
+
def auth_type(settings: Mapping[str, Any] | None) -> AuthenticationType | None:
|
176
179
|
"""Get the authentication type."""
|
177
180
|
if env_or_settings(settings, SECRET_ENV, SECRET_PROP, "") != "":
|
178
181
|
return AuthenticationType.SECRET
|
@@ -198,7 +201,9 @@ def auth_type(settings: Optional[Mapping[str, Any]]) -> Optional[AuthenticationT
|
|
198
201
|
|
199
202
|
|
200
203
|
def check_access(
|
201
|
-
request: pyramid.request.Request,
|
204
|
+
request: pyramid.request.Request,
|
205
|
+
repo: str | None = None,
|
206
|
+
access_type: str | None = None,
|
202
207
|
) -> bool:
|
203
208
|
"""
|
204
209
|
Check if the user has access to the resource.
|
@@ -274,7 +279,9 @@ def check_access_config(request: pyramid.request.Request, auth_config: AuthConfi
|
|
274
279
|
|
275
280
|
|
276
281
|
def is_enabled(
|
277
|
-
config: pyramid.config.Configurator,
|
282
|
+
config: pyramid.config.Configurator,
|
283
|
+
env_name: str | None = None,
|
284
|
+
config_name: str | None = None,
|
278
285
|
) -> bool:
|
279
286
|
"""Is the authentication enable."""
|
280
287
|
return (
|