c2cwsgiutils 1.0.0.dev1__tar.gz → 5.2.1.dev197__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-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/PKG-INFO +60 -30
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/README.md +33 -7
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/__init__.py +12 -12
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/acceptance/connection.py +5 -2
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/acceptance/image.py +94 -2
- c2cwsgiutils-5.2.1.dev197/c2cwsgiutils/acceptance/package-lock.json +1933 -0
- c2cwsgiutils-5.2.1.dev197/c2cwsgiutils/acceptance/package.json +7 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/acceptance/print.py +3 -3
- c2cwsgiutils-5.2.1.dev197/c2cwsgiutils/acceptance/screenshot.js +62 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/acceptance/utils.py +12 -20
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/auth.py +4 -3
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/broadcast/__init__.py +6 -6
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/broadcast/interface.py +3 -2
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/broadcast/local.py +3 -2
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/broadcast/redis.py +4 -3
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/client_info.py +5 -5
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/config_utils.py +2 -1
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/db.py +4 -2
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/db_maintenance_view.py +2 -1
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/debug/_listeners.py +5 -4
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/debug/_views.py +11 -10
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/debug/utils.py +5 -5
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/health_check.py +10 -22
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/index.py +90 -105
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/loader.py +3 -3
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/logging_view.py +3 -2
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/models_graph.py +4 -4
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/prometheus.py +16 -17
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/pyramid_logging.py +2 -1
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/redis_utils.py +3 -5
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/request_tracking/__init__.py +5 -4
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/scripts/genversion.py +4 -4
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/scripts/stats_db.py +13 -14
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sentry.py +2 -1
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/setup_process.py +3 -3
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sql_profiler/_impl.py +3 -2
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sqlalchemylogger/_models.py +2 -2
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sqlalchemylogger/handlers.py +3 -3
- c2cwsgiutils-5.2.1.dev197/c2cwsgiutils/static/favicon-16x16.png +0 -0
- c2cwsgiutils-5.2.1.dev197/c2cwsgiutils/static/favicon-32x32.png +0 -0
- c2cwsgiutils-5.2.1.dev197/c2cwsgiutils/templates/index.html.mako +50 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/version.py +5 -5
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/pyproject.toml +68 -67
- c2cwsgiutils-1.0.0.dev1/c2cwsgiutils/acceptance/composition.py +0 -148
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/LICENSE +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/acceptance/__init__.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/broadcast/utils.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/coverage_setup.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/debug/__init__.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/errors.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/pretty_json.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/profiler.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/py.typed +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/pyramid.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/redis_stats.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/request_tracking/_sql.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/scripts/__init__.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/scripts/test_print.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/services.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sql_profiler/__init__.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sqlalchemylogger/README.md +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sqlalchemylogger/__init__.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sqlalchemylogger/_filters.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/sqlalchemylogger/examples/example.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/stats_pyramid/__init__.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/stats_pyramid/_db_spy.py +0 -0
- {c2cwsgiutils-1.0.0.dev1 → c2cwsgiutils-5.2.1.dev197}/c2cwsgiutils/stats_pyramid/_pyramid_spy.py +0 -0
@@ -1,13 +1,13 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: c2cwsgiutils
|
3
|
-
Version:
|
3
|
+
Version: 5.2.1.dev197
|
4
4
|
Summary: Common utilities for Camptocamp WSGI applications
|
5
5
|
Home-page: https://github.com/camptocamp/c2cwsgiutils
|
6
6
|
License: BSD-2-Clause
|
7
7
|
Keywords: geo,gis,sqlalchemy,orm,wsgi
|
8
8
|
Author: Camptocamp
|
9
9
|
Author-email: info@camptocamp.com
|
10
|
-
Requires-Python: >=3.
|
10
|
+
Requires-Python: >=3.9,<4.0
|
11
11
|
Classifier: Development Status :: 5 - Production/Stable
|
12
12
|
Classifier: Environment :: Plugins
|
13
13
|
Classifier: Framework :: Pyramid
|
@@ -17,44 +17,48 @@ Classifier: License :: OSI Approved :: BSD License
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
18
18
|
Classifier: Programming Language :: Python
|
19
19
|
Classifier: Programming Language :: Python :: 3
|
20
|
-
Classifier: Programming Language :: Python :: 3.8
|
21
20
|
Classifier: Programming Language :: Python :: 3.9
|
22
21
|
Classifier: Programming Language :: Python :: 3.10
|
23
22
|
Classifier: Programming Language :: Python :: 3.11
|
24
23
|
Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application
|
25
24
|
Classifier: Typing :: Typed
|
25
|
+
Provides-Extra: alembic
|
26
26
|
Provides-Extra: all
|
27
27
|
Provides-Extra: broadcast
|
28
|
+
Provides-Extra: debug
|
28
29
|
Provides-Extra: dev
|
29
30
|
Provides-Extra: oauth2
|
31
|
+
Provides-Extra: sentry
|
30
32
|
Provides-Extra: standard
|
31
33
|
Provides-Extra: test-images
|
32
|
-
|
33
|
-
|
34
|
-
Requires-Dist:
|
35
|
-
Requires-Dist:
|
36
|
-
Requires-Dist:
|
34
|
+
Provides-Extra: tests
|
35
|
+
Provides-Extra: webserver
|
36
|
+
Requires-Dist: SQLAlchemy (>=1.4.0,<3.0.0)
|
37
|
+
Requires-Dist: SQLAlchemy-Utils
|
38
|
+
Requires-Dist: alembic ; extra == "standard" or extra == "alembic" or extra == "all"
|
39
|
+
Requires-Dist: boltons ; extra == "tests" or extra == "all"
|
40
|
+
Requires-Dist: cee_syslog_handler
|
37
41
|
Requires-Dist: certifi
|
38
|
-
Requires-Dist: cornice
|
39
|
-
Requires-Dist: gunicorn ; extra == "standard" or extra == "all"
|
40
|
-
Requires-Dist: lxml ; extra == "
|
41
|
-
Requires-Dist:
|
42
|
-
Requires-Dist: objgraph ; extra == "standard" or extra == "all"
|
42
|
+
Requires-Dist: cornice
|
43
|
+
Requires-Dist: gunicorn ; extra == "standard" or extra == "webserver" or extra == "all"
|
44
|
+
Requires-Dist: lxml ; extra == "tests" or extra == "all"
|
45
|
+
Requires-Dist: objgraph ; extra == "debug" or extra == "all"
|
43
46
|
Requires-Dist: prometheus-client
|
44
|
-
Requires-Dist: psycopg2
|
45
|
-
Requires-Dist: pyjwt ; extra == "oauth2" or extra == "all"
|
46
|
-
Requires-Dist: pyramid
|
47
|
-
Requires-Dist: pyramid-tm
|
47
|
+
Requires-Dist: psycopg2
|
48
|
+
Requires-Dist: pyjwt ; extra == "standard" or extra == "oauth2" or extra == "all"
|
49
|
+
Requires-Dist: pyramid
|
50
|
+
Requires-Dist: pyramid-tm
|
51
|
+
Requires-Dist: pyramid_mako
|
48
52
|
Requires-Dist: pyyaml
|
49
|
-
Requires-Dist: redis ; extra == "standard" or extra == "broadcast" or extra == "all"
|
53
|
+
Requires-Dist: redis ; extra == "standard" or extra == "broadcast" or extra == "all"
|
50
54
|
Requires-Dist: requests
|
51
|
-
Requires-Dist: requests-oauthlib ; extra == "oauth2" or extra == "all"
|
55
|
+
Requires-Dist: requests-oauthlib ; extra == "standard" or extra == "oauth2" or extra == "all"
|
52
56
|
Requires-Dist: scikit-image ; extra == "test-images"
|
53
|
-
Requires-Dist: sentry-sdk ; extra == "standard" or extra == "all"
|
54
|
-
Requires-Dist: ujson
|
57
|
+
Requires-Dist: sentry-sdk ; extra == "standard" or extra == "sentry" or extra == "all"
|
58
|
+
Requires-Dist: ujson
|
55
59
|
Requires-Dist: waitress ; extra == "dev" or extra == "all"
|
56
|
-
Requires-Dist: zope.interface
|
57
|
-
Requires-Dist: zope.sqlalchemy
|
60
|
+
Requires-Dist: zope.interface
|
61
|
+
Requires-Dist: zope.sqlalchemy
|
58
62
|
Project-URL: Repository, https://github.com/camptocamp/c2cwsgiutils
|
59
63
|
Description-Content-Type: text/markdown
|
60
64
|
|
@@ -120,7 +124,6 @@ ENV \
|
|
120
124
|
LOG_TYPE=console \
|
121
125
|
OTHER_LOG_LEVEL=WARNING \
|
122
126
|
GUNICORN_LOG_LEVEL=WARNING \
|
123
|
-
GUNICORN_ACCESS_LOG_LEVEL=INFO \
|
124
127
|
SQL_LOG_LEVEL=WARNING \
|
125
128
|
C2CWSGIUTILS_LOG_LEVEL=WARNING \
|
126
129
|
LOG_LEVEL=INFO
|
@@ -159,8 +162,6 @@ The related environment variables:
|
|
159
162
|
- `SQL_LOG_LEVEL`: The SQL query log level, `WARNING`: no logs, `INFO`: logs the queries,
|
160
163
|
`DEBUG` also logs the results, default is `WARNING`.
|
161
164
|
- `GUNICORN_ERROR_LOG_LEVEL`: The Gunicorn error log level, default is `WARNING`.
|
162
|
-
- `GUNICORN_ACCESS_LOG_LEVEL`: The Gunicorn access log level, the logs have the level `INFO`,
|
163
|
-
default is `WARNING`.
|
164
165
|
- `C2CWSGIUTILS_CONFIG`: The fallback ini file to use by gunicorn, default is `production.ini`.
|
165
166
|
- `C2CWSGIUTILS_LOG_LEVEL`: The c2c WSGI utils log level, default is `WARNING`.
|
166
167
|
- `OTHER_LOG_LEVEL`: The log level for all the other logger, default is `WARNING`.
|
@@ -483,14 +484,14 @@ command line. Usually done in the [Dockerfile](acceptance_tests/app/Dockerfile)
|
|
483
484
|
It will work in multi process mode with the limitation listed in the
|
484
485
|
[`prometheus_client` documentation](https://github.com/prometheus/client_python#multiprocess-mode-eg-gunicorn).
|
485
486
|
|
486
|
-
To enable it you should provide the `
|
487
|
+
To enable it you should provide the `C2CWSGIUTILS_PROMETHEUS_PORT` environment variable.
|
487
488
|
For security reason, this port should not be exposed.
|
488
489
|
|
489
490
|
We can customize it with the following environment variables:
|
490
491
|
|
491
|
-
- `
|
492
|
+
- `C2C_PROMETHEUS_PREFIX`: to customize the prefix, default is `c2cwsggiutils-`.
|
492
493
|
- `C2C_PROMETHEUS_PACKAGES` the packages that will be present in the version information, default is `c2cwsgiutils,pyramid,gunicorn,sqlalchemy`.
|
493
|
-
- `
|
494
|
+
- `C2C_PROMETHEUS_APPLICATION_PACKAGE` the packages that will be present in the version information as application.
|
494
495
|
|
495
496
|
And you should add in your `gunicorn.conf.py`:
|
496
497
|
|
@@ -524,7 +525,7 @@ In your `Dockerfile` you should add:
|
|
524
525
|
|
525
526
|
```dockerfile
|
526
527
|
RUN mkdir -p /prometheus-metrics \
|
527
|
-
chmod a+rwx /prometheus-metrics
|
528
|
+
&& chmod a+rwx /prometheus-metrics
|
528
529
|
ENV PROMETHEUS_MULTIPROC_DIR=/prometheus-metrics
|
529
530
|
```
|
530
531
|
|
@@ -757,6 +758,35 @@ To make a release:
|
|
757
758
|
- Add the new branch name in the `.github/workflows/rebuild.yaml` and
|
758
759
|
`.github/workflows/audit.yaml` files.
|
759
760
|
|
761
|
+
## Testing
|
762
|
+
|
763
|
+
### Screenshots
|
764
|
+
|
765
|
+
To test the screenshots, you need to install `node` with `npm`, to do that add the following lines in your `Dockerfile`:
|
766
|
+
|
767
|
+
```dockerfile
|
768
|
+
RUN --mount=type=cache,target=/var/lib/apt/lists \
|
769
|
+
--mount=type=cache,target=/var/cache,sharing=locked \
|
770
|
+
. /etc/os-release \
|
771
|
+
&& echo "deb https://deb.nodesource.com/node_18.x ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/nodesource.list \
|
772
|
+
&& curl --silent https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \
|
773
|
+
&& apt-get update \
|
774
|
+
&& apt-get install --assume-yes --no-install-recommends 'nodejs=18.*'
|
775
|
+
```
|
776
|
+
|
777
|
+
To do the image test call `check_screenshot` e.g.:
|
778
|
+
|
779
|
+
```python
|
780
|
+
def test_screenshot(app_connection):
|
781
|
+
image.check_screenshot(
|
782
|
+
app_connection.base_url + "my-path",
|
783
|
+
width=800,
|
784
|
+
height=600,
|
785
|
+
result_folder="results",
|
786
|
+
expected_filename=os.path.join(os.path.dirname(__file__), "my-check.expected.png"),
|
787
|
+
)
|
788
|
+
```
|
789
|
+
|
760
790
|
## Contributing
|
761
791
|
|
762
792
|
Install the pre-commit hooks:
|
@@ -60,7 +60,6 @@ ENV \
|
|
60
60
|
LOG_TYPE=console \
|
61
61
|
OTHER_LOG_LEVEL=WARNING \
|
62
62
|
GUNICORN_LOG_LEVEL=WARNING \
|
63
|
-
GUNICORN_ACCESS_LOG_LEVEL=INFO \
|
64
63
|
SQL_LOG_LEVEL=WARNING \
|
65
64
|
C2CWSGIUTILS_LOG_LEVEL=WARNING \
|
66
65
|
LOG_LEVEL=INFO
|
@@ -99,8 +98,6 @@ The related environment variables:
|
|
99
98
|
- `SQL_LOG_LEVEL`: The SQL query log level, `WARNING`: no logs, `INFO`: logs the queries,
|
100
99
|
`DEBUG` also logs the results, default is `WARNING`.
|
101
100
|
- `GUNICORN_ERROR_LOG_LEVEL`: The Gunicorn error log level, default is `WARNING`.
|
102
|
-
- `GUNICORN_ACCESS_LOG_LEVEL`: The Gunicorn access log level, the logs have the level `INFO`,
|
103
|
-
default is `WARNING`.
|
104
101
|
- `C2CWSGIUTILS_CONFIG`: The fallback ini file to use by gunicorn, default is `production.ini`.
|
105
102
|
- `C2CWSGIUTILS_LOG_LEVEL`: The c2c WSGI utils log level, default is `WARNING`.
|
106
103
|
- `OTHER_LOG_LEVEL`: The log level for all the other logger, default is `WARNING`.
|
@@ -423,14 +420,14 @@ command line. Usually done in the [Dockerfile](acceptance_tests/app/Dockerfile)
|
|
423
420
|
It will work in multi process mode with the limitation listed in the
|
424
421
|
[`prometheus_client` documentation](https://github.com/prometheus/client_python#multiprocess-mode-eg-gunicorn).
|
425
422
|
|
426
|
-
To enable it you should provide the `
|
423
|
+
To enable it you should provide the `C2CWSGIUTILS_PROMETHEUS_PORT` environment variable.
|
427
424
|
For security reason, this port should not be exposed.
|
428
425
|
|
429
426
|
We can customize it with the following environment variables:
|
430
427
|
|
431
|
-
- `
|
428
|
+
- `C2C_PROMETHEUS_PREFIX`: to customize the prefix, default is `c2cwsggiutils-`.
|
432
429
|
- `C2C_PROMETHEUS_PACKAGES` the packages that will be present in the version information, default is `c2cwsgiutils,pyramid,gunicorn,sqlalchemy`.
|
433
|
-
- `
|
430
|
+
- `C2C_PROMETHEUS_APPLICATION_PACKAGE` the packages that will be present in the version information as application.
|
434
431
|
|
435
432
|
And you should add in your `gunicorn.conf.py`:
|
436
433
|
|
@@ -464,7 +461,7 @@ In your `Dockerfile` you should add:
|
|
464
461
|
|
465
462
|
```dockerfile
|
466
463
|
RUN mkdir -p /prometheus-metrics \
|
467
|
-
chmod a+rwx /prometheus-metrics
|
464
|
+
&& chmod a+rwx /prometheus-metrics
|
468
465
|
ENV PROMETHEUS_MULTIPROC_DIR=/prometheus-metrics
|
469
466
|
```
|
470
467
|
|
@@ -697,6 +694,35 @@ To make a release:
|
|
697
694
|
- Add the new branch name in the `.github/workflows/rebuild.yaml` and
|
698
695
|
`.github/workflows/audit.yaml` files.
|
699
696
|
|
697
|
+
## Testing
|
698
|
+
|
699
|
+
### Screenshots
|
700
|
+
|
701
|
+
To test the screenshots, you need to install `node` with `npm`, to do that add the following lines in your `Dockerfile`:
|
702
|
+
|
703
|
+
```dockerfile
|
704
|
+
RUN --mount=type=cache,target=/var/lib/apt/lists \
|
705
|
+
--mount=type=cache,target=/var/cache,sharing=locked \
|
706
|
+
. /etc/os-release \
|
707
|
+
&& echo "deb https://deb.nodesource.com/node_18.x ${VERSION_CODENAME} main" > /etc/apt/sources.list.d/nodesource.list \
|
708
|
+
&& curl --silent https://deb.nodesource.com/gpgkey/nodesource.gpg.key | apt-key add - \
|
709
|
+
&& apt-get update \
|
710
|
+
&& apt-get install --assume-yes --no-install-recommends 'nodejs=18.*'
|
711
|
+
```
|
712
|
+
|
713
|
+
To do the image test call `check_screenshot` e.g.:
|
714
|
+
|
715
|
+
```python
|
716
|
+
def test_screenshot(app_connection):
|
717
|
+
image.check_screenshot(
|
718
|
+
app_connection.base_url + "my-path",
|
719
|
+
width=800,
|
720
|
+
height=600,
|
721
|
+
result_folder="results",
|
722
|
+
expected_filename=os.path.join(os.path.dirname(__file__), "my-check.expected.png"),
|
723
|
+
)
|
724
|
+
```
|
725
|
+
|
700
726
|
## Contributing
|
701
727
|
|
702
728
|
Install the pre-commit hooks:
|
@@ -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),
|
@@ -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,9 +132,10 @@ 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
141
|
expected, image_to_check, multichannel=True, full=True, channel_axis=2
|
@@ -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
|
+
)
|