c2cwsgiutils 6.2.0.dev35__tar.gz → 6.2.0.dev39__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.dev35 → c2cwsgiutils-6.2.0.dev39}/PKG-INFO +73 -12
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/README.md +58 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/__init__.py +1 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/connection.py +3 -2
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/image.py +5 -3
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/package-lock.json +13 -13
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/package.json +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/print.py +1 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/utils.py +2 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/auth.py +9 -8
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/broadcast/__init__.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/broadcast/local.py +4 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/broadcast/redis.py +8 -2
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/client_info.py +2 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/coverage_setup.py +2 -2
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/db.py +11 -2
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/db_maintenance_view.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/debug/__init__.py +4 -2
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/debug/_views.py +2 -3
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/errors.py +7 -2
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/health_check.py +39 -29
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/index.py +1 -1
- c2cwsgiutils-6.2.0.dev39/c2cwsgiutils/loader.py +39 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/logging_view.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/models_graph.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/pretty_json.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/profiler.py +1 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/prometheus.py +58 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/pyramid.py +1 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/pyramid_logging.py +4 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/redis_stats.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/redis_utils.py +2 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/request_tracking/__init__.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/scripts/genversion.py +4 -2
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/scripts/stats_db.py +1 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/scripts/test_print.py +4 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sentry.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/setup_process.py +5 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sql_profiler/__init__.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sql_profiler/_impl.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sqlalchemylogger/handlers.py +18 -12
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/stats_pyramid/__init__.py +2 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/stats_pyramid/_pyramid_spy.py +1 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/version.py +1 -1
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/pyproject.toml +31 -12
- c2cwsgiutils-6.2.0.dev35/c2cwsgiutils/loader.py +0 -21
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/LICENSE +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/screenshot.js +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/broadcast/interface.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/broadcast/utils.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/config_utils.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/debug/_listeners.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/debug/utils.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/py.typed +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/request_tracking/_sql.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/scripts/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/services.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sqlalchemylogger/README.md +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sqlalchemylogger/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sqlalchemylogger/_filters.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sqlalchemylogger/_models.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/sqlalchemylogger/examples/example.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/static/favicon-16x16.png +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/static/favicon-32x32.png +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/stats_pyramid/_db_spy.py +0 -0
- {c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/templates/index.html.mako +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: c2cwsgiutils
|
3
|
-
Version: 6.2.0.
|
3
|
+
Version: 6.2.0.dev39
|
4
4
|
Summary: Common utilities for Camptocamp WSGI applications
|
5
5
|
Home-page: https://github.com/camptocamp/c2cwsgiutils
|
6
6
|
License: BSD-2-Clause
|
@@ -33,23 +33,26 @@ Provides-Extra: sentry
|
|
33
33
|
Provides-Extra: standard
|
34
34
|
Provides-Extra: test-images
|
35
35
|
Provides-Extra: tests
|
36
|
+
Provides-Extra: waitress
|
36
37
|
Provides-Extra: webserver
|
37
|
-
Requires-Dist:
|
38
|
-
Requires-Dist: SQLAlchemy
|
38
|
+
Requires-Dist: Paste ; extra == "standard" or extra == "waitress" or extra == "all"
|
39
|
+
Requires-Dist: SQLAlchemy ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
40
|
+
Requires-Dist: SQLAlchemy-Utils ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
39
41
|
Requires-Dist: alembic ; extra == "standard" or extra == "alembic" or extra == "all"
|
40
42
|
Requires-Dist: boltons ; extra == "tests" or extra == "all"
|
41
43
|
Requires-Dist: cee_syslog_handler
|
42
|
-
Requires-Dist: cornice ; extra == "standard" or extra == "webserver" or extra == "all"
|
44
|
+
Requires-Dist: cornice ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
45
|
+
Requires-Dist: coverage ; extra == "debug" or extra == "all"
|
43
46
|
Requires-Dist: gunicorn ; extra == "standard" or extra == "webserver" or extra == "all"
|
44
47
|
Requires-Dist: lxml ; extra == "tests" or extra == "all"
|
45
48
|
Requires-Dist: objgraph ; extra == "debug" or extra == "all"
|
46
|
-
Requires-Dist: prometheus-client ; extra == "standard" or extra == "webserver" or extra == "all"
|
49
|
+
Requires-Dist: prometheus-client ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
47
50
|
Requires-Dist: psutil ; extra == "debug" or extra == "all"
|
48
|
-
Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "all"
|
51
|
+
Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
49
52
|
Requires-Dist: pyjwt ; extra == "standard" or extra == "oauth2" or extra == "all"
|
50
|
-
Requires-Dist: pyramid ; extra == "standard" or extra == "webserver" or extra == "all"
|
51
|
-
Requires-Dist: pyramid-tm ; extra == "standard" or extra == "webserver" or extra == "all"
|
52
|
-
Requires-Dist: pyramid_mako ; extra == "standard" or extra == "webserver" or extra == "all"
|
53
|
+
Requires-Dist: pyramid ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
54
|
+
Requires-Dist: pyramid-tm ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
55
|
+
Requires-Dist: pyramid_mako ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
53
56
|
Requires-Dist: pyyaml
|
54
57
|
Requires-Dist: redis ; extra == "standard" or extra == "broadcast" or extra == "all"
|
55
58
|
Requires-Dist: requests
|
@@ -57,9 +60,9 @@ Requires-Dist: requests-oauthlib ; extra == "standard" or extra == "oauth2" or e
|
|
57
60
|
Requires-Dist: scikit-image ; extra == "test-images"
|
58
61
|
Requires-Dist: sentry-sdk ; extra == "standard" or extra == "sentry" or extra == "all"
|
59
62
|
Requires-Dist: ujson
|
60
|
-
Requires-Dist: waitress ; extra == "dev" or extra == "all"
|
61
|
-
Requires-Dist: zope.interface ; extra == "standard" or extra == "webserver" or extra == "all"
|
62
|
-
Requires-Dist: zope.sqlalchemy ; extra == "standard" or extra == "webserver" or extra == "all"
|
63
|
+
Requires-Dist: waitress ; extra == "standard" or extra == "dev" or extra == "waitress" or extra == "all"
|
64
|
+
Requires-Dist: zope.interface ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
65
|
+
Requires-Dist: zope.sqlalchemy ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
|
63
66
|
Project-URL: Repository, https://github.com/camptocamp/c2cwsgiutils
|
64
67
|
Description-Content-Type: text/markdown
|
65
68
|
|
@@ -673,6 +676,64 @@ def hello_get(request):
|
|
673
676
|
return {'hello': True}
|
674
677
|
```
|
675
678
|
|
679
|
+
## Waitress
|
680
|
+
|
681
|
+
In production mode we usually use Gunicorn but we can also use Waitress.
|
682
|
+
|
683
|
+
The advantage to use Waitress is that it creates only one process, that makes it easier to manage especially on Kubernetes:
|
684
|
+
|
685
|
+
- The memory is more stable.
|
686
|
+
- The OOM killer will restart the container.
|
687
|
+
- Prometheus didn't request trick to aggregate the metrics.
|
688
|
+
|
689
|
+
Then to migrate from Gunicorn to Waitress you should do:
|
690
|
+
|
691
|
+
Add call to `c2cwsgiutils.prometheus.start_single_process()` on your application main function.
|
692
|
+
|
693
|
+
Changes to do in your docker file:
|
694
|
+
|
695
|
+
```diff
|
696
|
+
|
697
|
+
ENV \
|
698
|
+
- GUNICORN_LOG_LEVEL=WARNING \
|
699
|
+
+ WAITRESS_LOG_LEVEL=WARNING \
|
700
|
+
+ WAITRESS_THREADS=10 \
|
701
|
+
|
702
|
+
-RUN mkdir -p /prometheus-metrics \
|
703
|
+
- && chmod a+rwx /prometheus-metrics
|
704
|
+
-ENV PROMETHEUS_MULTIPROC_DIR=/prometheus-metrics
|
705
|
+
|
706
|
+
|
707
|
+
-CMD ["/venv/bin/gunicorn", "--paste=/app/production.ini"]
|
708
|
+
+CMD ["/venv/bin/pserve", "c2c:///app/production.ini"]
|
709
|
+
```
|
710
|
+
|
711
|
+
Remove the no more needed file `gunicorn.conf.py`.
|
712
|
+
|
713
|
+
Update the `production.ini` file:
|
714
|
+
|
715
|
+
```diff
|
716
|
+
|
717
|
+
-# this file should be used by gunicorn.
|
718
|
+
|
719
|
+
[server:main]
|
720
|
+
+threads = %(WAITRESS_THREADS)s
|
721
|
+
+trusted_proxy = True
|
722
|
+
+clear_untrusted_proxy_headers = False
|
723
|
+
|
724
|
+
[loggers]
|
725
|
+
-keys = root, gunicorn, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
|
726
|
+
+keys = root, waitress, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
|
727
|
+
|
728
|
+
-[logger_gunicorn]
|
729
|
+
-level = %(GUNICORN_LOG_LEVEL)s
|
730
|
+
+[logger_waitress]
|
731
|
+
+level = %(WAITRESS_LOG_LEVEL)s
|
732
|
+
handlers =
|
733
|
+
-qualname = gunicorn.error
|
734
|
+
+qualname = waitress
|
735
|
+
```
|
736
|
+
|
676
737
|
# Exception handling
|
677
738
|
|
678
739
|
c2cwsgiutils can install exception handling views that will catch any exception raised by the
|
@@ -608,6 +608,64 @@ def hello_get(request):
|
|
608
608
|
return {'hello': True}
|
609
609
|
```
|
610
610
|
|
611
|
+
## Waitress
|
612
|
+
|
613
|
+
In production mode we usually use Gunicorn but we can also use Waitress.
|
614
|
+
|
615
|
+
The advantage to use Waitress is that it creates only one process, that makes it easier to manage especially on Kubernetes:
|
616
|
+
|
617
|
+
- The memory is more stable.
|
618
|
+
- The OOM killer will restart the container.
|
619
|
+
- Prometheus didn't request trick to aggregate the metrics.
|
620
|
+
|
621
|
+
Then to migrate from Gunicorn to Waitress you should do:
|
622
|
+
|
623
|
+
Add call to `c2cwsgiutils.prometheus.start_single_process()` on your application main function.
|
624
|
+
|
625
|
+
Changes to do in your docker file:
|
626
|
+
|
627
|
+
```diff
|
628
|
+
|
629
|
+
ENV \
|
630
|
+
- GUNICORN_LOG_LEVEL=WARNING \
|
631
|
+
+ WAITRESS_LOG_LEVEL=WARNING \
|
632
|
+
+ WAITRESS_THREADS=10 \
|
633
|
+
|
634
|
+
-RUN mkdir -p /prometheus-metrics \
|
635
|
+
- && chmod a+rwx /prometheus-metrics
|
636
|
+
-ENV PROMETHEUS_MULTIPROC_DIR=/prometheus-metrics
|
637
|
+
|
638
|
+
|
639
|
+
-CMD ["/venv/bin/gunicorn", "--paste=/app/production.ini"]
|
640
|
+
+CMD ["/venv/bin/pserve", "c2c:///app/production.ini"]
|
641
|
+
```
|
642
|
+
|
643
|
+
Remove the no more needed file `gunicorn.conf.py`.
|
644
|
+
|
645
|
+
Update the `production.ini` file:
|
646
|
+
|
647
|
+
```diff
|
648
|
+
|
649
|
+
-# this file should be used by gunicorn.
|
650
|
+
|
651
|
+
[server:main]
|
652
|
+
+threads = %(WAITRESS_THREADS)s
|
653
|
+
+trusted_proxy = True
|
654
|
+
+clear_untrusted_proxy_headers = False
|
655
|
+
|
656
|
+
[loggers]
|
657
|
+
-keys = root, gunicorn, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
|
658
|
+
+keys = root, waitress, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
|
659
|
+
|
660
|
+
-[logger_gunicorn]
|
661
|
+
-level = %(GUNICORN_LOG_LEVEL)s
|
662
|
+
+[logger_waitress]
|
663
|
+
+level = %(WAITRESS_LOG_LEVEL)s
|
664
|
+
handlers =
|
665
|
+
-qualname = gunicorn.error
|
666
|
+
+qualname = waitress
|
667
|
+
```
|
668
|
+
|
611
669
|
# Exception handling
|
612
670
|
|
613
671
|
c2cwsgiutils can install exception handling views that will catch any exception raised by the
|
@@ -20,6 +20,7 @@ def retry(
|
|
20
20
|
tries: number of times to try (not retry) before giving up
|
21
21
|
delay: initial delay between retries in seconds
|
22
22
|
backoff: backoff multiplier e.g. value of 2 will double the delay each retry
|
23
|
+
|
23
24
|
"""
|
24
25
|
|
25
26
|
def deco_retry(f: typing.Callable[..., typing.Any]) -> typing.Callable[..., typing.Any]:
|
@@ -20,6 +20,7 @@ class Connection:
|
|
20
20
|
"""The connection."""
|
21
21
|
|
22
22
|
def __init__(self, base_url: str, origin: str) -> None:
|
23
|
+
"""Initialize the connection."""
|
23
24
|
self.base_url = base_url
|
24
25
|
if not self.base_url.endswith("/"):
|
25
26
|
self.base_url += "/"
|
@@ -93,10 +94,10 @@ class Connection:
|
|
93
94
|
check_response(r, expected_status, cache_expected=cache_expected)
|
94
95
|
self._check_cors(cors, r)
|
95
96
|
r.raw.decode_content = True
|
96
|
-
doc = etree.parse(r.raw) #
|
97
|
+
doc = etree.parse(r.raw) # noqa: S320
|
97
98
|
if schema is not None:
|
98
99
|
with open(schema, encoding="utf-8") as schema_file:
|
99
|
-
xml_schema = etree.XMLSchema(etree.parse(schema_file)) #
|
100
|
+
xml_schema = etree.XMLSchema(etree.parse(schema_file)) # noqa: S320
|
100
101
|
xml_schema.assertValid(doc)
|
101
102
|
return doc
|
102
103
|
|
@@ -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_extensions import TypeAlias
|
14
14
|
|
15
15
|
NpNdarrayInt: TypeAlias = np.ndarray[np.uint8, Any]
|
16
16
|
else:
|
@@ -89,6 +89,7 @@ def check_image(
|
|
89
89
|
level: The minimum similarity level (between 0.0 and 1.0), default to 1.0
|
90
90
|
generate_expected_image: If `True` generate the expected image instead of checking it
|
91
91
|
use_mask: If `False` don't use the mask event if the file exists
|
92
|
+
|
92
93
|
"""
|
93
94
|
assert image_to_check is not None, "Image required"
|
94
95
|
image_file_basename = os.path.splitext(os.path.basename(expected_filename))[0]
|
@@ -122,7 +123,7 @@ def check_image(
|
|
122
123
|
if np.issubdtype(mask.dtype, np.floating):
|
123
124
|
mask = (mask * 255).astype("uint8")
|
124
125
|
|
125
|
-
assert ((
|
126
|
+
assert ((mask > 0) & (mask < 255)).sum() == 0, "Mask should be only black and white image"
|
126
127
|
|
127
128
|
# Convert to boolean
|
128
129
|
mask = mask == 0
|
@@ -139,7 +140,7 @@ def check_image(
|
|
139
140
|
return
|
140
141
|
if not os.path.isfile(expected_filename):
|
141
142
|
skimage.io.imsave(expected_filename, image_to_check)
|
142
|
-
|
143
|
+
raise AssertionError("Expected image not found: " + expected_filename)
|
143
144
|
expected = skimage.io.imread(expected_filename)
|
144
145
|
assert expected is not None, "Wrong image: " + expected_filename
|
145
146
|
expected = normalize_image(expected)
|
@@ -201,6 +202,7 @@ def check_screenshot(
|
|
201
202
|
level: See `check_image`
|
202
203
|
generate_expected_image: See `check_image`
|
203
204
|
use_mask: See `check_image`
|
205
|
+
|
204
206
|
"""
|
205
207
|
if headers is None:
|
206
208
|
headers = {}
|
{c2cwsgiutils-6.2.0.dev35 → c2cwsgiutils-6.2.0.dev39}/c2cwsgiutils/acceptance/package-lock.json
RENAMED
@@ -6,7 +6,7 @@
|
|
6
6
|
"": {
|
7
7
|
"dependencies": {
|
8
8
|
"commander": "12.1.0",
|
9
|
-
"puppeteer": "23.
|
9
|
+
"puppeteer": "23.9.0"
|
10
10
|
}
|
11
11
|
},
|
12
12
|
"node_modules/@babel/code-frame": {
|
@@ -379,9 +379,9 @@
|
|
379
379
|
}
|
380
380
|
},
|
381
381
|
"node_modules/devtools-protocol": {
|
382
|
-
"version": "0.0.
|
383
|
-
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.
|
384
|
-
"integrity": "sha512-
|
382
|
+
"version": "0.0.1367902",
|
383
|
+
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
|
384
|
+
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
|
385
385
|
"license": "BSD-3-Clause"
|
386
386
|
},
|
387
387
|
"node_modules/emoji-regex": {
|
@@ -868,17 +868,17 @@
|
|
868
868
|
}
|
869
869
|
},
|
870
870
|
"node_modules/puppeteer": {
|
871
|
-
"version": "23.
|
872
|
-
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.
|
873
|
-
"integrity": "sha512-
|
871
|
+
"version": "23.9.0",
|
872
|
+
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.9.0.tgz",
|
873
|
+
"integrity": "sha512-WfB8jGwFV+qrD9dcJJVvWPFJBU6kxeu2wxJz9WooDGfM3vIiKLgzImEDBxUQnCBK/2cXB3d4dV6gs/LLpgfLDg==",
|
874
874
|
"hasInstallScript": true,
|
875
875
|
"license": "Apache-2.0",
|
876
876
|
"dependencies": {
|
877
877
|
"@puppeteer/browsers": "2.4.1",
|
878
878
|
"chromium-bidi": "0.8.0",
|
879
879
|
"cosmiconfig": "^9.0.0",
|
880
|
-
"devtools-protocol": "0.0.
|
881
|
-
"puppeteer-core": "23.
|
880
|
+
"devtools-protocol": "0.0.1367902",
|
881
|
+
"puppeteer-core": "23.9.0",
|
882
882
|
"typed-query-selector": "^2.12.0"
|
883
883
|
},
|
884
884
|
"bin": {
|
@@ -889,15 +889,15 @@
|
|
889
889
|
}
|
890
890
|
},
|
891
891
|
"node_modules/puppeteer-core": {
|
892
|
-
"version": "23.
|
893
|
-
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.
|
894
|
-
"integrity": "sha512-
|
892
|
+
"version": "23.9.0",
|
893
|
+
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.9.0.tgz",
|
894
|
+
"integrity": "sha512-hLVrav2HYMVdK0YILtfJwtnkBAwNOztUdR4aJ5YKDvgsbtagNr6urUJk9HyjRA9e+PaLI3jzJ0wM7A4jSZ7Qxw==",
|
895
895
|
"license": "Apache-2.0",
|
896
896
|
"dependencies": {
|
897
897
|
"@puppeteer/browsers": "2.4.1",
|
898
898
|
"chromium-bidi": "0.8.0",
|
899
899
|
"debug": "^4.3.7",
|
900
|
-
"devtools-protocol": "0.0.
|
900
|
+
"devtools-protocol": "0.0.1367902",
|
901
901
|
"typed-query-selector": "^2.12.0",
|
902
902
|
"ws": "^8.18.0"
|
903
903
|
},
|
@@ -21,6 +21,7 @@ class PrintConnection(connection.Connection):
|
|
21
21
|
base_url: The base URL to the print server (including the /print)
|
22
22
|
app: The name of the application to use
|
23
23
|
origin: The origin and referrer to include in the requests
|
24
|
+
|
24
25
|
"""
|
25
26
|
super().__init__(base_url=base_url, origin=origin)
|
26
27
|
self.session.headers["Referrer"] = origin
|
@@ -32,6 +32,7 @@ def retry_timeout(what: Callable[[], Any], timeout: float = _DEFAULT_TIMEOUT, in
|
|
32
32
|
what: the function to try
|
33
33
|
timeout: the timeout to get a success
|
34
34
|
interval: the interval between try
|
35
|
+
|
35
36
|
"""
|
36
37
|
timeout = time.perf_counter() + timeout
|
37
38
|
while True:
|
@@ -46,7 +47,7 @@ def retry_timeout(what: Callable[[], Any], timeout: float = _DEFAULT_TIMEOUT, in
|
|
46
47
|
error = str(e)
|
47
48
|
_LOG.info(" Failed: %s", e)
|
48
49
|
if time.perf_counter() > timeout:
|
49
|
-
|
50
|
+
raise AssertionError("Timeout: " + error)
|
50
51
|
time.sleep(interval)
|
51
52
|
|
52
53
|
|
@@ -12,32 +12,32 @@ from requests_oauthlib import OAuth2Session
|
|
12
12
|
from c2cwsgiutils.config_utils import config_bool, env_or_config, env_or_settings
|
13
13
|
|
14
14
|
_COOKIE_AGE = 7 * 24 * 3600
|
15
|
-
SECRET_PROP = "c2c.secret" #
|
16
|
-
SECRET_ENV = "C2C_SECRET" #
|
15
|
+
SECRET_PROP = "c2c.secret" # noqa: S105
|
16
|
+
SECRET_ENV = "C2C_SECRET" # noqa: S105
|
17
17
|
_GITHUB_REPOSITORY_PROP = "c2c.auth.github.repository"
|
18
18
|
_GITHUB_REPOSITORY_ENV = "C2C_AUTH_GITHUB_REPOSITORY"
|
19
19
|
_GITHUB_ACCESS_TYPE_PROP = "c2c.auth.github.access_type"
|
20
20
|
_GITHUB_ACCESS_TYPE_ENV = "C2C_AUTH_GITHUB_ACCESS_TYPE"
|
21
21
|
GITHUB_AUTH_URL_PROP = "c2c.auth.github.auth_url"
|
22
22
|
GITHUB_AUTH_URL_ENV = "C2C_AUTH_GITHUB_AUTH_URL"
|
23
|
-
GITHUB_TOKEN_URL_PROP = "c2c.auth.github.token_url" #
|
24
|
-
GITHUB_TOKEN_URL_ENV = "C2C_AUTH_GITHUB_TOKEN_URL" #
|
23
|
+
GITHUB_TOKEN_URL_PROP = "c2c.auth.github.token_url" # noqa: S105
|
24
|
+
GITHUB_TOKEN_URL_ENV = "C2C_AUTH_GITHUB_TOKEN_URL" # noqa: S105
|
25
25
|
GITHUB_USER_URL_PROP = "c2c.auth.github.user_url"
|
26
26
|
GITHUB_USER_URL_ENV = "C2C_AUTH_GITHUB_USER_URL"
|
27
27
|
_GITHUB_REPO_URL_PROP = "c2c.auth.github.repo_url"
|
28
28
|
_GITHUB_REPO_URL_ENV = "C2C_AUTH_GITHUB_REPO_URL"
|
29
29
|
GITHUB_CLIENT_ID_PROP = "c2c.auth.github.client_id"
|
30
30
|
GITHUB_CLIENT_ID_ENV = "C2C_AUTH_GITHUB_CLIENT_ID"
|
31
|
-
GITHUB_CLIENT_SECRET_PROP = "c2c.auth.github.client_secret" #
|
32
|
-
GITHUB_CLIENT_SECRET_ENV = "C2C_AUTH_GITHUB_CLIENT_SECRET" #
|
31
|
+
GITHUB_CLIENT_SECRET_PROP = "c2c.auth.github.client_secret" # noqa: S105
|
32
|
+
GITHUB_CLIENT_SECRET_ENV = "C2C_AUTH_GITHUB_CLIENT_SECRET" # noqa: S105
|
33
33
|
GITHUB_SCOPE_PROP = "c2c.auth.github.scope"
|
34
34
|
GITHUB_SCOPE_ENV = "C2C_AUTH_GITHUB_SCOPE"
|
35
35
|
# To be able to use private repository
|
36
36
|
GITHUB_SCOPE_DEFAULT = "repo"
|
37
37
|
GITHUB_AUTH_COOKIE_PROP = "c2c.auth.github.auth.cookie"
|
38
38
|
GITHUB_AUTH_COOKIE_ENV = "C2C_AUTH_GITHUB_COOKIE"
|
39
|
-
GITHUB_AUTH_SECRET_PROP = "c2c.auth.github.auth.secret" #
|
40
|
-
GITHUB_AUTH_SECRET_ENV = "C2C_AUTH_GITHUB_SECRET" #
|
39
|
+
GITHUB_AUTH_SECRET_PROP = "c2c.auth.github.auth.secret" # noqa: S105
|
40
|
+
GITHUB_AUTH_SECRET_ENV = "C2C_AUTH_GITHUB_SECRET" # noqa: S105
|
41
41
|
GITHUB_AUTH_PROXY_URL_PROP = "c2c.auth.github.auth.proxy_url"
|
42
42
|
GITHUB_AUTH_PROXY_URL_ENV = "C2C_AUTH_GITHUB_PROXY_URL"
|
43
43
|
USE_SESSION_PROP = "c2c.use_session"
|
@@ -209,6 +209,7 @@ def check_access(
|
|
209
209
|
request: is the request object.
|
210
210
|
repo: is the repository to check access to (<organization>/<repository>).
|
211
211
|
access_type: is the type of access to check (admin|push|pull).
|
212
|
+
|
212
213
|
"""
|
213
214
|
if not is_auth(request):
|
214
215
|
return False
|
@@ -19,7 +19,7 @@ _broadcaster: Optional[interface.BaseBroadcaster] = None
|
|
19
19
|
|
20
20
|
def init(config: Optional[pyramid.config.Configurator] = None) -> None:
|
21
21
|
"""Initialize the broadcaster with Redis, if configured, for backward compatibility."""
|
22
|
-
warnings.warn("init function is deprecated; use includeme instead")
|
22
|
+
warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
|
23
23
|
includeme(config)
|
24
24
|
|
25
25
|
|
@@ -9,17 +9,21 @@ class LocalBroadcaster(interface.BaseBroadcaster):
|
|
9
9
|
"""Fake implementation of broadcasting messages (will just answer locally)."""
|
10
10
|
|
11
11
|
def __init__(self) -> None:
|
12
|
+
"""Initialize the broadcaster."""
|
12
13
|
self._subscribers: MutableMapping[str, Callable[..., Any]] = {}
|
13
14
|
|
14
15
|
def subscribe(self, channel: str, callback: Callable[..., Any]) -> None:
|
16
|
+
"""Subscribe to a channel."""
|
15
17
|
self._subscribers[channel] = callback
|
16
18
|
|
17
19
|
def unsubscribe(self, channel: str) -> None:
|
20
|
+
"""Unsubscribe from a channel."""
|
18
21
|
del self._subscribers[channel]
|
19
22
|
|
20
23
|
def broadcast(
|
21
24
|
self, channel: str, params: Mapping[str, Any], expect_answers: bool, timeout: float
|
22
25
|
) -> Optional[list[Any]]:
|
26
|
+
"""Broadcast a message to all the listeners."""
|
23
27
|
subscriber = self._subscribers.get(channel, None)
|
24
28
|
answers = [utils.add_host_info(subscriber(**params))] if subscriber is not None else []
|
25
29
|
return answers if expect_answers else None
|
@@ -23,6 +23,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
23
23
|
master: "redis.client.Redis[str]",
|
24
24
|
slave: "redis.client.Redis[str]",
|
25
25
|
) -> None:
|
26
|
+
"""Initialize the broadcaster."""
|
26
27
|
from c2cwsgiutils import redis_utils # pylint: disable=import-outside-toplevel
|
27
28
|
|
28
29
|
self._master = master
|
@@ -40,6 +41,8 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
40
41
|
return self._broadcast_prefix + channel
|
41
42
|
|
42
43
|
def subscribe(self, channel: str, callback: Callable[..., Any]) -> None:
|
44
|
+
"""Subscribe to a channel."""
|
45
|
+
|
43
46
|
def wrapper(message: Mapping[str, Any]) -> None:
|
44
47
|
_LOG.debug("Received a broadcast on %s: %s", message["channel"], repr(message["data"]))
|
45
48
|
data = json.loads(message["data"])
|
@@ -58,6 +61,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
58
61
|
self._pub_sub.subscribe(**{actual_channel: wrapper})
|
59
62
|
|
60
63
|
def unsubscribe(self, channel: str) -> None:
|
64
|
+
"""Unsubscribe from a channel."""
|
61
65
|
_LOG.debug("Unsubscribing from %s")
|
62
66
|
actual_channel = self._get_channel(channel)
|
63
67
|
self._pub_sub.unsubscribe(actual_channel)
|
@@ -65,6 +69,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
65
69
|
def broadcast(
|
66
70
|
self, channel: str, params: Mapping[str, Any], expect_answers: bool, timeout: float
|
67
71
|
) -> Optional[list[Any]]:
|
72
|
+
"""Broadcast a message to all the listeners."""
|
68
73
|
if expect_answers:
|
69
74
|
return self._broadcast_with_answer(channel, params, timeout)
|
70
75
|
else:
|
@@ -85,7 +90,8 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
85
90
|
cond.notify()
|
86
91
|
|
87
92
|
answer_channel = self._get_channel(channel) + "".join(
|
88
|
-
random.choice(string.ascii_uppercase + string.digits)
|
93
|
+
random.choice(string.ascii_uppercase + string.digits) # noqa: S311
|
94
|
+
for _ in range(10)
|
89
95
|
)
|
90
96
|
_LOG.debug("Subscribing for broadcast answers on %s", answer_channel)
|
91
97
|
self._pub_sub.subscribe(**{answer_channel: callback})
|
@@ -98,7 +104,7 @@ class RedisBroadcaster(interface.BaseBroadcaster):
|
|
98
104
|
with cond:
|
99
105
|
while len(answers) < nb_received:
|
100
106
|
to_wait = timeout_time - time.perf_counter()
|
101
|
-
if to_wait <= 0.0:
|
107
|
+
if to_wait <= 0.0:
|
102
108
|
_LOG.warning(
|
103
109
|
"timeout waiting for %d/%d answers on %s",
|
104
110
|
len(answers),
|
@@ -16,9 +16,11 @@ class Filter:
|
|
16
16
|
"""
|
17
17
|
|
18
18
|
def __init__(self, application: Callable[[dict[str, str], Any], Any]):
|
19
|
+
"""Initialize the filter."""
|
19
20
|
self._application = application
|
20
21
|
|
21
22
|
def __call__(self, environ: dict[str, str], start_response: Any) -> Any:
|
23
|
+
"""Update the environ with the headers."""
|
22
24
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
|
23
25
|
if "HTTP_FORWARDED" in environ:
|
24
26
|
_handle_forwarded(environ)
|
@@ -10,7 +10,7 @@ _LOG = logging.getLogger(__name__)
|
|
10
10
|
|
11
11
|
def init() -> None:
|
12
12
|
"""Initialize the code coverage, for backward compatibility."""
|
13
|
-
warnings.warn("init function is deprecated; use includeme instead")
|
13
|
+
warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
|
14
14
|
includeme()
|
15
15
|
|
16
16
|
|
@@ -22,7 +22,7 @@ def includeme(config: Optional[pyramid.config.Configurator] = None) -> None:
|
|
22
22
|
import coverage # pylint: disable=import-outside-toplevel
|
23
23
|
|
24
24
|
_LOG.warning("Setting up code coverage")
|
25
|
-
report_dir = "/tmp/coverage/api" #
|
25
|
+
report_dir = "/tmp/coverage/api" # noqa: S108
|
26
26
|
os.makedirs(report_dir, exist_ok=True)
|
27
27
|
cov = coverage.Coverage(
|
28
28
|
data_file=os.path.join(report_dir, "coverage"),
|
@@ -66,8 +66,11 @@ def setup_session(
|
|
66
66
|
force_slave: The method/paths that needs to use the slave
|
67
67
|
|
68
68
|
Returns: The SQLAlchemy session, the R/W engine and the R/O engine
|
69
|
+
|
69
70
|
"""
|
70
|
-
warnings.warn(
|
71
|
+
warnings.warn(
|
72
|
+
"setup_session function is deprecated; use init and request.dbsession instead", stacklevel=2
|
73
|
+
)
|
71
74
|
if slave_prefix is None:
|
72
75
|
slave_prefix = master_prefix
|
73
76
|
settings = config.registry.settings
|
@@ -122,8 +125,11 @@ def create_session(
|
|
122
125
|
engine_config: The rest of the parameters are passed as is to the sqlalchemy.create_engine function
|
123
126
|
|
124
127
|
Returns: The SQLAlchemy session
|
128
|
+
|
125
129
|
"""
|
126
|
-
warnings.warn(
|
130
|
+
warnings.warn(
|
131
|
+
"create_session function is deprecated; use init and request.dbsession instead", stacklevel=2
|
132
|
+
)
|
127
133
|
if slave_url is None:
|
128
134
|
slave_url = url
|
129
135
|
|
@@ -215,6 +221,7 @@ class SessionFactory(_sessionmaker):
|
|
215
221
|
ro_engine: sqlalchemy.engine.Engine,
|
216
222
|
rw_engine: sqlalchemy.engine.Engine,
|
217
223
|
):
|
224
|
+
"""Initialize the session factory."""
|
218
225
|
super().__init__()
|
219
226
|
self.master_paths: Iterable[Pattern[str]] = (
|
220
227
|
list(map(_RE_COMPILE, force_master)) if force_master else []
|
@@ -232,6 +239,7 @@ class SessionFactory(_sessionmaker):
|
|
232
239
|
def __call__( # type: ignore
|
233
240
|
self, request: Optional[pyramid.request.Request], readwrite: Optional[bool] = None, **local_kw: Any
|
234
241
|
) -> _scoped_session:
|
242
|
+
"""Set the engine based on the request."""
|
235
243
|
if readwrite is not None:
|
236
244
|
if readwrite and not FORCE_READONLY:
|
237
245
|
_LOG.debug("Using %s database", self.rw_engine.c2c_name) # type: ignore
|
@@ -374,6 +382,7 @@ def init(
|
|
374
382
|
force_slave: The method/paths that needs to use the slave
|
375
383
|
|
376
384
|
Returns: The SQLAlchemy session
|
385
|
+
|
377
386
|
"""
|
378
387
|
settings = config.get_settings()
|
379
388
|
settings["tm.manager_hook"] = "pyramid_tm.explicit_manager"
|
@@ -15,7 +15,7 @@ _REDIS_PREFIX = "c2c_db_maintenance_"
|
|
15
15
|
|
16
16
|
def install_subscriber(config: pyramid.config.Configurator) -> None:
|
17
17
|
"""Install the view to configure the loggers, if configured to do so, for backward compatibility."""
|
18
|
-
warnings.warn("install_subscriber function is deprecated; use includeme instead")
|
18
|
+
warnings.warn("install_subscriber function is deprecated; use includeme instead", stacklevel=2)
|
19
19
|
includeme(config)
|
20
20
|
|
21
21
|
|
@@ -16,7 +16,7 @@ dump_memory_maps = utils.dump_memory_maps
|
|
16
16
|
|
17
17
|
def init(config: pyramid.config.Configurator) -> None:
|
18
18
|
"""Initialize the debug tools, for backward compatibility."""
|
19
|
-
warnings.warn("init function is deprecated; use includeme instead")
|
19
|
+
warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
|
20
20
|
includeme(config)
|
21
21
|
|
22
22
|
|
@@ -37,6 +37,8 @@ def init_daemon(config: Optional[pyramid.config.Configurator] = None) -> None:
|
|
37
37
|
those requests.
|
38
38
|
"""
|
39
39
|
if config_utils.env_or_config(config, ENV_KEY, CONFIG_KEY, type_=config_utils.config_bool):
|
40
|
-
from c2cwsgiutils.debug import
|
40
|
+
from c2cwsgiutils.debug import ( # pylint: disable=import-outside-toplevel
|
41
|
+
_listeners,
|
42
|
+
)
|
41
43
|
|
42
44
|
_listeners.init()
|
@@ -79,7 +79,7 @@ def _dump_memory_diff(request: pyramid.request.Request) -> list[Any]:
|
|
79
79
|
try:
|
80
80
|
if request.params.get("no_warmup", "0").lower() in ("1", "true", "on"):
|
81
81
|
request.invoke_subrequest(sub_request)
|
82
|
-
except Exception: #
|
82
|
+
except Exception: # pylint: disable=broad-except
|
83
83
|
pass
|
84
84
|
|
85
85
|
_LOG.debug("checking memory growth for %s", path)
|
@@ -151,8 +151,7 @@ def _headers(request: pyramid.request.Request) -> Mapping[str, Any]:
|
|
151
151
|
}
|
152
152
|
if "status" in request.params:
|
153
153
|
raise exception_response(int(request.params["status"]), detail=result)
|
154
|
-
|
155
|
-
return result
|
154
|
+
return result
|
156
155
|
|
157
156
|
|
158
157
|
def _error(request: pyramid.request.Request) -> Any:
|
@@ -9,7 +9,12 @@ from typing import Any, Callable
|
|
9
9
|
import pyramid.request
|
10
10
|
import sqlalchemy.exc
|
11
11
|
from cornice import cors
|
12
|
-
from pyramid.httpexceptions import
|
12
|
+
from pyramid.httpexceptions import (
|
13
|
+
HTTPError,
|
14
|
+
HTTPException,
|
15
|
+
HTTPRedirection,
|
16
|
+
HTTPSuccessful,
|
17
|
+
)
|
13
18
|
from webob.request import DisconnectionError
|
14
19
|
|
15
20
|
from c2cwsgiutils import auth, config_utils
|
@@ -152,7 +157,7 @@ def _passthrough(exception: HTTPException, request: pyramid.request.Request) ->
|
|
152
157
|
|
153
158
|
def init(config: pyramid.config.Configurator) -> None:
|
154
159
|
"""Initialize the error views, for backward compatibility."""
|
155
|
-
warnings.warn("init function is deprecated; use includeme instead")
|
160
|
+
warnings.warn("init function is deprecated; use includeme instead", stacklevel=2)
|
156
161
|
includeme(config)
|
157
162
|
|
158
163
|
|