c2cwsgiutils 6.2.0.dev20__tar.gz → 6.2.0.dev207__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.dev20 → c2cwsgiutils-6.2.0.dev207}/LICENSE +1 -1
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/PKG-INFO +136 -24
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/README.md +58 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/__init__.py +4 -4
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/__init__.py +5 -1
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/connection.py +48 -34
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/image.py +66 -51
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/package-lock.json +222 -308
- c2cwsgiutils-6.2.0.dev207/c2cwsgiutils/acceptance/package.json +7 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/print.py +3 -2
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/utils.py +6 -5
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/auth.py +30 -22
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/__init__.py +29 -20
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/interface.py +8 -4
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/local.py +13 -4
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/redis.py +27 -14
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/client_info.py +5 -2
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/config_utils.py +15 -11
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/coverage_setup.py +6 -6
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/db.py +70 -51
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/db_maintenance_view.py +9 -10
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/debug/__init__.py +5 -4
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/debug/_listeners.py +8 -11
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/debug/_views.py +50 -20
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/debug/utils.py +5 -5
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/errors.py +22 -12
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/health_check.py +114 -85
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/index.py +65 -44
- c2cwsgiutils-6.2.0.dev207/c2cwsgiutils/loader.py +41 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/logging_view.py +14 -7
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/models_graph.py +6 -6
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/pretty_json.py +4 -3
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/profiler.py +2 -2
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/prometheus.py +74 -9
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/pyramid.py +2 -1
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/pyramid_logging.py +13 -4
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/redis_stats.py +14 -9
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/redis_utils.py +22 -12
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/request_tracking/__init__.py +22 -14
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/request_tracking/_sql.py +2 -1
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/scripts/genversion.py +14 -11
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/scripts/stats_db.py +32 -15
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/scripts/test_print.py +5 -1
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sentry.py +46 -22
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/setup_process.py +12 -5
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sql_profiler/__init__.py +1 -1
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sql_profiler/_impl.py +13 -12
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/_models.py +3 -3
- c2cwsgiutils-6.2.0.dev207/c2cwsgiutils/sqlalchemylogger/examples/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/handlers.py +21 -16
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/stats_pyramid/__init__.py +2 -1
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/stats_pyramid/_db_spy.py +19 -7
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/stats_pyramid/_pyramid_spy.py +23 -9
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/templates/index.html.mako +6 -3
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/version.py +12 -9
- c2cwsgiutils-6.2.0.dev207/pyproject.toml +263 -0
- c2cwsgiutils-6.2.0.dev20/c2cwsgiutils/acceptance/package.json +0 -7
- c2cwsgiutils-6.2.0.dev20/c2cwsgiutils/loader.py +0 -21
- c2cwsgiutils-6.2.0.dev20/pyproject.toml +0 -214
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/screenshot.js +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/utils.py +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/py.typed +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/scripts/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/services.py +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/README.md +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/__init__.py +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/_filters.py +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/examples/example.py +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/static/favicon-16x16.png +0 -0
- {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/static/favicon-32x32.png +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: c2cwsgiutils
|
|
3
|
-
Version: 6.2.0.
|
|
3
|
+
Version: 6.2.0.dev207
|
|
4
4
|
Summary: Common utilities for Camptocamp WSGI applications
|
|
5
|
-
|
|
6
|
-
License:
|
|
5
|
+
License-Expression: BSD-2-Clause
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Keywords: geo,gis,sqlalchemy,orm,wsgi
|
|
8
8
|
Author: Camptocamp
|
|
9
9
|
Author-email: info@camptocamp.com
|
|
@@ -33,32 +33,86 @@ 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:
|
|
39
|
-
Requires-Dist:
|
|
40
|
-
Requires-Dist:
|
|
38
|
+
Requires-Dist: Paste ; extra == "all"
|
|
39
|
+
Requires-Dist: Paste ; extra == "standard"
|
|
40
|
+
Requires-Dist: Paste ; extra == "waitress"
|
|
41
|
+
Requires-Dist: SQLAlchemy (>=1.4.0,<3.0.0) ; extra == "all"
|
|
42
|
+
Requires-Dist: SQLAlchemy (>=1.4.0,<3.0.0) ; extra == "standard"
|
|
43
|
+
Requires-Dist: SQLAlchemy (>=1.4.0,<3.0.0) ; extra == "waitress"
|
|
44
|
+
Requires-Dist: SQLAlchemy (>=1.4.0,<3.0.0) ; extra == "webserver"
|
|
45
|
+
Requires-Dist: SQLAlchemy-Utils ; extra == "all"
|
|
46
|
+
Requires-Dist: SQLAlchemy-Utils ; extra == "standard"
|
|
47
|
+
Requires-Dist: SQLAlchemy-Utils ; extra == "waitress"
|
|
48
|
+
Requires-Dist: SQLAlchemy-Utils ; extra == "webserver"
|
|
49
|
+
Requires-Dist: alembic ; extra == "alembic"
|
|
50
|
+
Requires-Dist: alembic ; extra == "all"
|
|
51
|
+
Requires-Dist: alembic ; extra == "standard"
|
|
52
|
+
Requires-Dist: boltons ; extra == "all"
|
|
53
|
+
Requires-Dist: boltons ; extra == "tests"
|
|
41
54
|
Requires-Dist: cee_syslog_handler
|
|
42
|
-
Requires-Dist: cornice ; extra == "
|
|
43
|
-
Requires-Dist:
|
|
44
|
-
Requires-Dist:
|
|
45
|
-
Requires-Dist:
|
|
46
|
-
Requires-Dist:
|
|
47
|
-
Requires-Dist:
|
|
48
|
-
Requires-Dist:
|
|
49
|
-
Requires-Dist:
|
|
50
|
-
Requires-Dist:
|
|
51
|
-
Requires-Dist:
|
|
55
|
+
Requires-Dist: cornice ; extra == "all"
|
|
56
|
+
Requires-Dist: cornice ; extra == "standard"
|
|
57
|
+
Requires-Dist: cornice ; extra == "waitress"
|
|
58
|
+
Requires-Dist: cornice ; extra == "webserver"
|
|
59
|
+
Requires-Dist: coverage ; extra == "all"
|
|
60
|
+
Requires-Dist: coverage ; extra == "debug"
|
|
61
|
+
Requires-Dist: gunicorn ; extra == "all"
|
|
62
|
+
Requires-Dist: gunicorn ; extra == "standard"
|
|
63
|
+
Requires-Dist: gunicorn ; extra == "webserver"
|
|
64
|
+
Requires-Dist: lxml ; extra == "all"
|
|
65
|
+
Requires-Dist: lxml ; extra == "tests"
|
|
66
|
+
Requires-Dist: objgraph ; extra == "all"
|
|
67
|
+
Requires-Dist: objgraph ; extra == "debug"
|
|
68
|
+
Requires-Dist: prometheus-client
|
|
69
|
+
Requires-Dist: psutil ; extra == "all"
|
|
70
|
+
Requires-Dist: psutil ; extra == "debug"
|
|
71
|
+
Requires-Dist: psycopg2 ; extra == "all"
|
|
72
|
+
Requires-Dist: psycopg2 ; extra == "standard"
|
|
73
|
+
Requires-Dist: psycopg2 ; extra == "waitress"
|
|
74
|
+
Requires-Dist: psycopg2 ; extra == "webserver"
|
|
75
|
+
Requires-Dist: pyjwt ; extra == "all"
|
|
76
|
+
Requires-Dist: pyjwt ; extra == "oauth2"
|
|
77
|
+
Requires-Dist: pyjwt ; extra == "standard"
|
|
78
|
+
Requires-Dist: pyramid ; extra == "all"
|
|
79
|
+
Requires-Dist: pyramid ; extra == "standard"
|
|
80
|
+
Requires-Dist: pyramid ; extra == "waitress"
|
|
81
|
+
Requires-Dist: pyramid ; extra == "webserver"
|
|
82
|
+
Requires-Dist: pyramid-tm ; extra == "all"
|
|
83
|
+
Requires-Dist: pyramid-tm ; extra == "standard"
|
|
84
|
+
Requires-Dist: pyramid-tm ; extra == "waitress"
|
|
85
|
+
Requires-Dist: pyramid-tm ; extra == "webserver"
|
|
86
|
+
Requires-Dist: pyramid_mako ; extra == "all"
|
|
87
|
+
Requires-Dist: pyramid_mako ; extra == "standard"
|
|
88
|
+
Requires-Dist: pyramid_mako ; extra == "waitress"
|
|
89
|
+
Requires-Dist: pyramid_mako ; extra == "webserver"
|
|
52
90
|
Requires-Dist: pyyaml
|
|
53
|
-
Requires-Dist: redis ; extra == "
|
|
91
|
+
Requires-Dist: redis ; extra == "all"
|
|
92
|
+
Requires-Dist: redis ; extra == "broadcast"
|
|
93
|
+
Requires-Dist: redis ; extra == "standard"
|
|
54
94
|
Requires-Dist: requests
|
|
55
|
-
Requires-Dist: requests-oauthlib ; extra == "
|
|
95
|
+
Requires-Dist: requests-oauthlib ; extra == "all"
|
|
96
|
+
Requires-Dist: requests-oauthlib ; extra == "oauth2"
|
|
97
|
+
Requires-Dist: requests-oauthlib ; extra == "standard"
|
|
56
98
|
Requires-Dist: scikit-image ; extra == "test-images"
|
|
57
|
-
Requires-Dist: sentry-sdk ; extra == "
|
|
99
|
+
Requires-Dist: sentry-sdk ; extra == "all"
|
|
100
|
+
Requires-Dist: sentry-sdk ; extra == "sentry"
|
|
101
|
+
Requires-Dist: sentry-sdk ; extra == "standard"
|
|
58
102
|
Requires-Dist: ujson
|
|
59
|
-
Requires-Dist: waitress ; extra == "
|
|
60
|
-
Requires-Dist:
|
|
61
|
-
Requires-Dist:
|
|
103
|
+
Requires-Dist: waitress ; extra == "all"
|
|
104
|
+
Requires-Dist: waitress ; extra == "dev"
|
|
105
|
+
Requires-Dist: waitress ; extra == "standard"
|
|
106
|
+
Requires-Dist: waitress ; extra == "waitress"
|
|
107
|
+
Requires-Dist: zope.interface ; extra == "all"
|
|
108
|
+
Requires-Dist: zope.interface ; extra == "standard"
|
|
109
|
+
Requires-Dist: zope.interface ; extra == "waitress"
|
|
110
|
+
Requires-Dist: zope.interface ; extra == "webserver"
|
|
111
|
+
Requires-Dist: zope.sqlalchemy ; extra == "all"
|
|
112
|
+
Requires-Dist: zope.sqlalchemy ; extra == "standard"
|
|
113
|
+
Requires-Dist: zope.sqlalchemy ; extra == "waitress"
|
|
114
|
+
Requires-Dist: zope.sqlalchemy ; extra == "webserver"
|
|
115
|
+
Project-URL: Bug Tracker, https://github.com/camptocamp/c2cwsgiutils/issues
|
|
62
116
|
Project-URL: Repository, https://github.com/camptocamp/c2cwsgiutils
|
|
63
117
|
Description-Content-Type: text/markdown
|
|
64
118
|
|
|
@@ -672,6 +726,64 @@ def hello_get(request):
|
|
|
672
726
|
return {'hello': True}
|
|
673
727
|
```
|
|
674
728
|
|
|
729
|
+
## Waitress
|
|
730
|
+
|
|
731
|
+
In production mode we usually use Gunicorn but we can also use Waitress.
|
|
732
|
+
|
|
733
|
+
The advantage to use Waitress is that it creates only one process, that makes it easier to manage especially on Kubernetes:
|
|
734
|
+
|
|
735
|
+
- The memory is more stable.
|
|
736
|
+
- The OOM killer will restart the container.
|
|
737
|
+
- Prometheus didn't request trick to aggregate the metrics.
|
|
738
|
+
|
|
739
|
+
Then to migrate from Gunicorn to Waitress you should do:
|
|
740
|
+
|
|
741
|
+
Add call to `c2cwsgiutils.prometheus.start_single_process()` on your application main function.
|
|
742
|
+
|
|
743
|
+
Changes to do in your docker file:
|
|
744
|
+
|
|
745
|
+
```diff
|
|
746
|
+
|
|
747
|
+
ENV \
|
|
748
|
+
- GUNICORN_LOG_LEVEL=WARNING \
|
|
749
|
+
+ WAITRESS_LOG_LEVEL=WARNING \
|
|
750
|
+
+ WAITRESS_THREADS=10 \
|
|
751
|
+
|
|
752
|
+
-RUN mkdir -p /prometheus-metrics \
|
|
753
|
+
- && chmod a+rwx /prometheus-metrics
|
|
754
|
+
-ENV PROMETHEUS_MULTIPROC_DIR=/prometheus-metrics
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
-CMD ["/venv/bin/gunicorn", "--paste=/app/production.ini"]
|
|
758
|
+
+CMD ["/venv/bin/pserve", "c2c:///app/production.ini"]
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
Remove the no more needed file `gunicorn.conf.py`.
|
|
762
|
+
|
|
763
|
+
Update the `production.ini` file:
|
|
764
|
+
|
|
765
|
+
```diff
|
|
766
|
+
|
|
767
|
+
-# this file should be used by gunicorn.
|
|
768
|
+
|
|
769
|
+
[server:main]
|
|
770
|
+
+threads = %(WAITRESS_THREADS)s
|
|
771
|
+
+trusted_proxy = True
|
|
772
|
+
+clear_untrusted_proxy_headers = False
|
|
773
|
+
|
|
774
|
+
[loggers]
|
|
775
|
+
-keys = root, gunicorn, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
|
|
776
|
+
+keys = root, waitress, sqlalchemy, c2cwsgiutils, c2cwsgiutils_app
|
|
777
|
+
|
|
778
|
+
-[logger_gunicorn]
|
|
779
|
+
-level = %(GUNICORN_LOG_LEVEL)s
|
|
780
|
+
+[logger_waitress]
|
|
781
|
+
+level = %(WAITRESS_LOG_LEVEL)s
|
|
782
|
+
handlers =
|
|
783
|
+
-qualname = gunicorn.error
|
|
784
|
+
+qualname = waitress
|
|
785
|
+
```
|
|
786
|
+
|
|
675
787
|
# Exception handling
|
|
676
788
|
|
|
677
789
|
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
|
|
@@ -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.
|
|
@@ -20,6 +23,7 @@ def retry(
|
|
|
20
23
|
tries: number of times to try (not retry) before giving up
|
|
21
24
|
delay: initial delay between retries in seconds
|
|
22
25
|
backoff: backoff multiplier e.g. value of 2 will double the delay each retry
|
|
26
|
+
|
|
23
27
|
"""
|
|
24
28
|
|
|
25
29
|
def deco_retry(f: typing.Callable[..., typing.Any]) -> typing.Callable[..., typing.Any]:
|
{c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/connection.py
RENAMED
|
@@ -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
|
|
|
@@ -20,6 +21,7 @@ class Connection:
|
|
|
20
21
|
"""The connection."""
|
|
21
22
|
|
|
22
23
|
def __init__(self, base_url: str, origin: str) -> None:
|
|
24
|
+
"""Initialize the connection."""
|
|
23
25
|
self.base_url = base_url
|
|
24
26
|
if not self.base_url.endswith("/"):
|
|
25
27
|
self.base_url += "/"
|
|
@@ -31,10 +33,10 @@ class Connection:
|
|
|
31
33
|
url: str,
|
|
32
34
|
expected_status: int = 200,
|
|
33
35
|
cors: bool = True,
|
|
34
|
-
headers:
|
|
36
|
+
headers: Mapping[str, str] | None = None,
|
|
35
37
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
36
38
|
**kwargs: Any,
|
|
37
|
-
) ->
|
|
39
|
+
) -> str | None:
|
|
38
40
|
"""Get the given URL (relative to the root of API)."""
|
|
39
41
|
with self.session.get(self.base_url + url, headers=self._merge_headers(headers, cors), **kwargs) as r:
|
|
40
42
|
check_response(r, expected_status, cache_expected=cache_expected)
|
|
@@ -45,7 +47,7 @@ class Connection:
|
|
|
45
47
|
self,
|
|
46
48
|
url: str,
|
|
47
49
|
expected_status: int = 200,
|
|
48
|
-
headers:
|
|
50
|
+
headers: Mapping[str, str] | None = None,
|
|
49
51
|
cors: bool = True,
|
|
50
52
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
51
53
|
**kwargs: Any,
|
|
@@ -60,7 +62,7 @@ class Connection:
|
|
|
60
62
|
self,
|
|
61
63
|
url: str,
|
|
62
64
|
expected_status: int = 200,
|
|
63
|
-
headers:
|
|
65
|
+
headers: Mapping[str, str] | None = None,
|
|
64
66
|
cors: bool = True,
|
|
65
67
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
66
68
|
**kwargs: Any,
|
|
@@ -74,9 +76,9 @@ class Connection:
|
|
|
74
76
|
def get_xml(
|
|
75
77
|
self,
|
|
76
78
|
url: str,
|
|
77
|
-
schema:
|
|
79
|
+
schema: Path | None = None,
|
|
78
80
|
expected_status: int = 200,
|
|
79
|
-
headers:
|
|
81
|
+
headers: Mapping[str, str] | None = None,
|
|
80
82
|
cors: bool = True,
|
|
81
83
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
82
84
|
**kwargs: Any,
|
|
@@ -93,10 +95,10 @@ class Connection:
|
|
|
93
95
|
check_response(r, expected_status, cache_expected=cache_expected)
|
|
94
96
|
self._check_cors(cors, r)
|
|
95
97
|
r.raw.decode_content = True
|
|
96
|
-
doc = etree.parse(r.raw) #
|
|
98
|
+
doc = etree.parse(r.raw) # noqa: S320, RUF100
|
|
97
99
|
if schema is not None:
|
|
98
|
-
with open(
|
|
99
|
-
xml_schema = etree.XMLSchema(etree.parse(schema_file)) #
|
|
100
|
+
with schema.open(encoding="utf-8") as schema_file:
|
|
101
|
+
xml_schema = etree.XMLSchema(etree.parse(schema_file)) # noqa: S320, RUF100
|
|
100
102
|
xml_schema.assertValid(doc)
|
|
101
103
|
return doc
|
|
102
104
|
|
|
@@ -104,14 +106,16 @@ class Connection:
|
|
|
104
106
|
self,
|
|
105
107
|
url: str,
|
|
106
108
|
expected_status: int = 200,
|
|
107
|
-
headers:
|
|
109
|
+
headers: Mapping[str, str] | None = None,
|
|
108
110
|
cors: bool = True,
|
|
109
111
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
110
112
|
**kwargs: Any,
|
|
111
113
|
) -> Any:
|
|
112
114
|
"""POST the given URL (relative to the root of API)."""
|
|
113
115
|
with self.session.post(
|
|
114
|
-
self.base_url + url,
|
|
116
|
+
self.base_url + url,
|
|
117
|
+
headers=self._merge_headers(headers, cors),
|
|
118
|
+
**kwargs,
|
|
115
119
|
) as r:
|
|
116
120
|
check_response(r, expected_status, cache_expected=cache_expected)
|
|
117
121
|
self._check_cors(cors, r)
|
|
@@ -121,14 +125,16 @@ class Connection:
|
|
|
121
125
|
self,
|
|
122
126
|
url: str,
|
|
123
127
|
expected_status: int = 200,
|
|
124
|
-
headers:
|
|
128
|
+
headers: Mapping[str, str] | None = None,
|
|
125
129
|
cors: bool = True,
|
|
126
130
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
127
131
|
**kwargs: Any,
|
|
128
132
|
) -> Any:
|
|
129
133
|
"""POST files to the the given URL (relative to the root of API)."""
|
|
130
134
|
with self.session.post(
|
|
131
|
-
self.base_url + url,
|
|
135
|
+
self.base_url + url,
|
|
136
|
+
headers=self._merge_headers(headers, cors),
|
|
137
|
+
**kwargs,
|
|
132
138
|
) as r:
|
|
133
139
|
check_response(r, expected_status, cache_expected)
|
|
134
140
|
self._check_cors(cors, r)
|
|
@@ -138,14 +144,16 @@ class Connection:
|
|
|
138
144
|
self,
|
|
139
145
|
url: str,
|
|
140
146
|
expected_status: int = 200,
|
|
141
|
-
headers:
|
|
147
|
+
headers: Mapping[str, str] | None = None,
|
|
142
148
|
cors: bool = True,
|
|
143
149
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
144
150
|
**kwargs: Any,
|
|
145
|
-
) ->
|
|
151
|
+
) -> str | None:
|
|
146
152
|
"""POST the given URL (relative to the root of API)."""
|
|
147
153
|
with self.session.post(
|
|
148
|
-
self.base_url + url,
|
|
154
|
+
self.base_url + url,
|
|
155
|
+
headers=self._merge_headers(headers, cors),
|
|
156
|
+
**kwargs,
|
|
149
157
|
) as r:
|
|
150
158
|
check_response(r, expected_status, cache_expected)
|
|
151
159
|
self._check_cors(cors, r)
|
|
@@ -155,7 +163,7 @@ class Connection:
|
|
|
155
163
|
self,
|
|
156
164
|
url: str,
|
|
157
165
|
expected_status: int = 200,
|
|
158
|
-
headers:
|
|
166
|
+
headers: Mapping[str, str] | None = None,
|
|
159
167
|
cors: bool = True,
|
|
160
168
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
161
169
|
**kwargs: Any,
|
|
@@ -170,14 +178,16 @@ class Connection:
|
|
|
170
178
|
self,
|
|
171
179
|
url: str,
|
|
172
180
|
expected_status: int = 200,
|
|
173
|
-
headers:
|
|
181
|
+
headers: Mapping[str, str] | None = None,
|
|
174
182
|
cors: bool = True,
|
|
175
183
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
176
184
|
**kwargs: Any,
|
|
177
185
|
) -> Any:
|
|
178
186
|
"""PATCH the given URL (relative to the root of API)."""
|
|
179
187
|
with self.session.patch(
|
|
180
|
-
self.base_url + url,
|
|
188
|
+
self.base_url + url,
|
|
189
|
+
headers=self._merge_headers(headers, cors),
|
|
190
|
+
**kwargs,
|
|
181
191
|
) as r:
|
|
182
192
|
check_response(r, expected_status, cache_expected)
|
|
183
193
|
self._check_cors(cors, r)
|
|
@@ -187,14 +197,16 @@ class Connection:
|
|
|
187
197
|
self,
|
|
188
198
|
url: str,
|
|
189
199
|
expected_status: int = 204,
|
|
190
|
-
headers:
|
|
200
|
+
headers: Mapping[str, str] | None = None,
|
|
191
201
|
cors: bool = True,
|
|
192
202
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
193
203
|
**kwargs: Any,
|
|
194
204
|
) -> requests.Response:
|
|
195
205
|
"""DELETE the given URL (relative to the root of API)."""
|
|
196
206
|
with self.session.delete(
|
|
197
|
-
self.base_url + url,
|
|
207
|
+
self.base_url + url,
|
|
208
|
+
headers=self._merge_headers(headers, cors),
|
|
209
|
+
**kwargs,
|
|
198
210
|
) as r:
|
|
199
211
|
check_response(r, expected_status, cache_expected)
|
|
200
212
|
self._check_cors(cors, r)
|
|
@@ -204,13 +216,15 @@ class Connection:
|
|
|
204
216
|
self,
|
|
205
217
|
url: str,
|
|
206
218
|
expected_status: int = 200,
|
|
207
|
-
headers:
|
|
219
|
+
headers: Mapping[str, str] | None = None,
|
|
208
220
|
cache_expected: CacheExpected = CacheExpected.NO,
|
|
209
221
|
**kwargs: Any,
|
|
210
222
|
) -> requests.Response:
|
|
211
223
|
"""Get the given URL (relative to the root of API)."""
|
|
212
224
|
with self.session.options(
|
|
213
|
-
self.base_url + url,
|
|
225
|
+
self.base_url + url,
|
|
226
|
+
headers=self._merge_headers(headers, cors=False),
|
|
227
|
+
**kwargs,
|
|
214
228
|
) as r:
|
|
215
229
|
check_response(r, expected_status, cache_expected=cache_expected)
|
|
216
230
|
return r
|
|
@@ -218,8 +232,7 @@ class Connection:
|
|
|
218
232
|
def _cors_headers(self, cors: bool) -> Mapping[str, str]:
|
|
219
233
|
if cors:
|
|
220
234
|
return {"Origin": self.origin}
|
|
221
|
-
|
|
222
|
-
return {}
|
|
235
|
+
return {}
|
|
223
236
|
|
|
224
237
|
def _check_cors(self, cors: bool, r: requests.Response) -> None:
|
|
225
238
|
if cors:
|
|
@@ -229,8 +242,10 @@ class Connection:
|
|
|
229
242
|
assert r.headers["Access-Control-Allow-Origin"] == "*"
|
|
230
243
|
|
|
231
244
|
def _merge_headers(
|
|
232
|
-
self,
|
|
233
|
-
|
|
245
|
+
self,
|
|
246
|
+
headers: Mapping[str, str | bytes] | None,
|
|
247
|
+
cors: bool,
|
|
248
|
+
) -> MutableMapping[str, str | bytes]:
|
|
234
249
|
merged = dict(headers) if headers is not None else {}
|
|
235
250
|
if self.session.headers is not None:
|
|
236
251
|
merged.update(self.session.headers)
|
|
@@ -264,9 +279,8 @@ def check_response(
|
|
|
264
279
|
def _get_json(r: requests.Response) -> Any:
|
|
265
280
|
if r.status_code == 204:
|
|
266
281
|
return None
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
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()
|