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.
Files changed (70) hide show
  1. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/LICENSE +1 -1
  2. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/PKG-INFO +136 -24
  3. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/README.md +58 -0
  4. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/__init__.py +4 -4
  5. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/__init__.py +5 -1
  6. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/connection.py +48 -34
  7. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/image.py +66 -51
  8. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/package-lock.json +222 -308
  9. c2cwsgiutils-6.2.0.dev207/c2cwsgiutils/acceptance/package.json +7 -0
  10. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/print.py +3 -2
  11. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/utils.py +6 -5
  12. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/auth.py +30 -22
  13. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/__init__.py +29 -20
  14. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/interface.py +8 -4
  15. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/local.py +13 -4
  16. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/redis.py +27 -14
  17. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/client_info.py +5 -2
  18. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/config_utils.py +15 -11
  19. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/coverage_setup.py +6 -6
  20. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/db.py +70 -51
  21. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/db_maintenance_view.py +9 -10
  22. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/debug/__init__.py +5 -4
  23. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/debug/_listeners.py +8 -11
  24. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/debug/_views.py +50 -20
  25. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/debug/utils.py +5 -5
  26. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/errors.py +22 -12
  27. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/health_check.py +114 -85
  28. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/index.py +65 -44
  29. c2cwsgiutils-6.2.0.dev207/c2cwsgiutils/loader.py +41 -0
  30. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/logging_view.py +14 -7
  31. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/models_graph.py +6 -6
  32. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/pretty_json.py +4 -3
  33. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/profiler.py +2 -2
  34. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/prometheus.py +74 -9
  35. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/pyramid.py +2 -1
  36. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/pyramid_logging.py +13 -4
  37. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/redis_stats.py +14 -9
  38. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/redis_utils.py +22 -12
  39. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/request_tracking/__init__.py +22 -14
  40. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/request_tracking/_sql.py +2 -1
  41. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/scripts/genversion.py +14 -11
  42. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/scripts/stats_db.py +32 -15
  43. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/scripts/test_print.py +5 -1
  44. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sentry.py +46 -22
  45. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/setup_process.py +12 -5
  46. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sql_profiler/__init__.py +1 -1
  47. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sql_profiler/_impl.py +13 -12
  48. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/_models.py +3 -3
  49. c2cwsgiutils-6.2.0.dev207/c2cwsgiutils/sqlalchemylogger/examples/__init__.py +0 -0
  50. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/handlers.py +21 -16
  51. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/stats_pyramid/__init__.py +2 -1
  52. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/stats_pyramid/_db_spy.py +19 -7
  53. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/stats_pyramid/_pyramid_spy.py +23 -9
  54. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/templates/index.html.mako +6 -3
  55. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/version.py +12 -9
  56. c2cwsgiutils-6.2.0.dev207/pyproject.toml +263 -0
  57. c2cwsgiutils-6.2.0.dev20/c2cwsgiutils/acceptance/package.json +0 -7
  58. c2cwsgiutils-6.2.0.dev20/c2cwsgiutils/loader.py +0 -21
  59. c2cwsgiutils-6.2.0.dev20/pyproject.toml +0 -214
  60. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/acceptance/screenshot.js +0 -0
  61. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/broadcast/utils.py +0 -0
  62. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/py.typed +0 -0
  63. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/scripts/__init__.py +0 -0
  64. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/services.py +0 -0
  65. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/README.md +0 -0
  66. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/__init__.py +0 -0
  67. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/_filters.py +0 -0
  68. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/sqlalchemylogger/examples/example.py +0 -0
  69. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/static/favicon-16x16.png +0 -0
  70. {c2cwsgiutils-6.2.0.dev20 → c2cwsgiutils-6.2.0.dev207}/c2cwsgiutils/static/favicon-32x32.png +0 -0
@@ -1,4 +1,4 @@
1
- Copyright (c) 2015-2024, Camptocamp SA
1
+ Copyright (c) 2015-2026, Camptocamp SA
2
2
  All rights reserved.
3
3
 
4
4
  Redistribution and use in source and binary forms, with or without
@@ -1,9 +1,9 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: c2cwsgiutils
3
- Version: 6.2.0.dev20
3
+ Version: 6.2.0.dev207
4
4
  Summary: Common utilities for Camptocamp WSGI applications
5
- Home-page: https://github.com/camptocamp/c2cwsgiutils
6
- License: BSD-2-Clause
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: SQLAlchemy ; extra == "standard" or extra == "webserver" or extra == "all"
38
- Requires-Dist: SQLAlchemy-Utils ; extra == "standard" or extra == "webserver" or extra == "all"
39
- Requires-Dist: alembic ; extra == "standard" or extra == "alembic" or extra == "all"
40
- Requires-Dist: boltons ; extra == "tests" or extra == "all"
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 == "standard" or extra == "webserver" or extra == "all"
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"
46
- Requires-Dist: prometheus-client ; extra == "standard" or extra == "webserver" or extra == "all"
47
- Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "all"
48
- Requires-Dist: pyjwt ; extra == "standard" or extra == "oauth2" or extra == "all"
49
- Requires-Dist: pyramid ; extra == "standard" or extra == "webserver" or extra == "all"
50
- Requires-Dist: pyramid-tm ; extra == "standard" or extra == "webserver" or extra == "all"
51
- Requires-Dist: pyramid_mako ; extra == "standard" or extra == "webserver" or extra == "all"
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 == "standard" or extra == "broadcast" or extra == "all"
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 == "standard" or extra == "oauth2" or extra == "all"
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 == "standard" or extra == "sentry" or extra == "all"
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 == "dev" or extra == "all"
60
- Requires-Dist: zope.interface ; extra == "standard" or extra == "webserver" or extra == "all"
61
- Requires-Dist: zope.sqlalchemy ; extra == "standard" or extra == "webserver" or extra == "all"
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
- raise ValueError(f"Can not parse args of handlers {hh}, use kwargs instead.")
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=") or val.startswith("--paster="):
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
- fallback = os.environ.get("C2CWSGIUTILS_CONFIG", "production.ini")
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, tries: float = 3, delay: float = 0.5, backoff: float = 2
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]:
@@ -1,7 +1,8 @@
1
1
  import re
2
2
  from collections.abc import Mapping, MutableMapping
3
3
  from enum import Enum
4
- from typing import Any, Optional, Union
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: Optional[Mapping[str, str]] = None,
36
+ headers: Mapping[str, str] | None = None,
35
37
  cache_expected: CacheExpected = CacheExpected.NO,
36
38
  **kwargs: Any,
37
- ) -> Optional[str]:
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: Optional[Mapping[str, str]] = None,
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: Optional[Mapping[str, str]] = None,
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: Optional[str] = None,
79
+ schema: Path | None = None,
78
80
  expected_status: int = 200,
79
- headers: Optional[Mapping[str, str]] = None,
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) # nosec
98
+ doc = etree.parse(r.raw) # noqa: S320, RUF100
97
99
  if schema is not None:
98
- with open(schema, encoding="utf-8") as schema_file:
99
- xml_schema = etree.XMLSchema(etree.parse(schema_file)) # nosec
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: Optional[Mapping[str, str]] = None,
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, headers=self._merge_headers(headers, cors), **kwargs
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: Optional[Mapping[str, str]] = None,
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, headers=self._merge_headers(headers, cors), **kwargs
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: Optional[Mapping[str, str]] = None,
147
+ headers: Mapping[str, str] | None = None,
142
148
  cors: bool = True,
143
149
  cache_expected: CacheExpected = CacheExpected.NO,
144
150
  **kwargs: Any,
145
- ) -> Optional[str]:
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, headers=self._merge_headers(headers, cors), **kwargs
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: Optional[Mapping[str, str]] = None,
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: Optional[Mapping[str, str]] = None,
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, headers=self._merge_headers(headers, cors), **kwargs
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: Optional[Mapping[str, str]] = None,
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, headers=self._merge_headers(headers, cors), **kwargs
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: Optional[Mapping[str, str]] = None,
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, headers=self._merge_headers(headers, False), **kwargs
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
- else:
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, headers: Optional[Mapping[str, Union[str, bytes]]], cors: bool
233
- ) -> MutableMapping[str, Union[str, bytes]]:
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
- else:
268
- content_type = r.headers["Content-Type"].split(";")[0]
269
- assert content_type == "application/json" or content_type.endswith(
270
- "+json"
271
- ), f"{r.status_code}, {content_type}, {r.text}"
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()