c2cwsgiutils 6.1.7.dev4__tar.gz → 6.2.0.dev54__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 (67) hide show
  1. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/PKG-INFO +74 -12
  2. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/README.md +58 -0
  3. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/acceptance/__init__.py +1 -0
  4. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/acceptance/connection.py +3 -2
  5. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/acceptance/image.py +5 -3
  6. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/acceptance/package-lock.json +110 -248
  7. c2cwsgiutils-6.2.0.dev54/c2cwsgiutils/acceptance/package.json +7 -0
  8. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/acceptance/print.py +1 -0
  9. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/acceptance/utils.py +2 -1
  10. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/auth.py +9 -8
  11. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/broadcast/__init__.py +1 -1
  12. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/broadcast/local.py +4 -0
  13. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/broadcast/redis.py +8 -2
  14. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/client_info.py +2 -0
  15. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/coverage_setup.py +2 -2
  16. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/db.py +11 -2
  17. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/db_maintenance_view.py +1 -1
  18. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/debug/__init__.py +4 -2
  19. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/debug/_views.py +22 -4
  20. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/errors.py +7 -2
  21. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/health_check.py +39 -29
  22. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/index.py +1 -1
  23. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/loader.py +1 -1
  24. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/logging_view.py +1 -1
  25. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/models_graph.py +1 -1
  26. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/pretty_json.py +1 -1
  27. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/profiler.py +1 -0
  28. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/prometheus.py +58 -0
  29. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/pyramid.py +1 -0
  30. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/pyramid_logging.py +4 -0
  31. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/redis_stats.py +1 -1
  32. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/redis_utils.py +2 -0
  33. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/request_tracking/__init__.py +1 -1
  34. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/scripts/genversion.py +4 -2
  35. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/scripts/stats_db.py +1 -0
  36. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/scripts/test_print.py +4 -1
  37. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sentry.py +1 -1
  38. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/setup_process.py +5 -1
  39. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sql_profiler/__init__.py +1 -1
  40. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sql_profiler/_impl.py +1 -1
  41. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sqlalchemylogger/handlers.py +18 -12
  42. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/stats_pyramid/__init__.py +2 -1
  43. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/stats_pyramid/_pyramid_spy.py +1 -0
  44. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/version.py +1 -1
  45. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/pyproject.toml +40 -19
  46. c2cwsgiutils-6.1.7.dev4/c2cwsgiutils/acceptance/package.json +0 -7
  47. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/LICENSE +0 -0
  48. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/__init__.py +0 -0
  49. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/acceptance/screenshot.js +0 -0
  50. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/broadcast/interface.py +0 -0
  51. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/broadcast/utils.py +0 -0
  52. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/config_utils.py +0 -0
  53. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/debug/_listeners.py +0 -0
  54. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/debug/utils.py +0 -0
  55. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/py.typed +0 -0
  56. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/request_tracking/_sql.py +0 -0
  57. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/scripts/__init__.py +0 -0
  58. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/services.py +0 -0
  59. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sqlalchemylogger/README.md +0 -0
  60. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sqlalchemylogger/__init__.py +0 -0
  61. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sqlalchemylogger/_filters.py +0 -0
  62. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sqlalchemylogger/_models.py +0 -0
  63. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/sqlalchemylogger/examples/example.py +0 -0
  64. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/static/favicon-16x16.png +0 -0
  65. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/static/favicon-32x32.png +0 -0
  66. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/stats_pyramid/_db_spy.py +0 -0
  67. {c2cwsgiutils-6.1.7.dev4 → c2cwsgiutils-6.2.0.dev54}/c2cwsgiutils/templates/index.html.mako +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: c2cwsgiutils
3
- Version: 6.1.7.dev4
3
+ Version: 6.2.0.dev54
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,22 +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: SQLAlchemy ; extra == "standard" or extra == "webserver" or extra == "all"
38
- Requires-Dist: SQLAlchemy-Utils ; extra == "standard" or extra == "webserver" or extra == "all"
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"
47
- Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "all"
49
+ Requires-Dist: prometheus-client ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
50
+ Requires-Dist: psutil ; extra == "debug" or extra == "all"
51
+ Requires-Dist: psycopg2 ; extra == "standard" or extra == "webserver" or extra == "waitress" or extra == "all"
48
52
  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"
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"
52
56
  Requires-Dist: pyyaml
53
57
  Requires-Dist: redis ; extra == "standard" or extra == "broadcast" or extra == "all"
54
58
  Requires-Dist: requests
@@ -56,9 +60,9 @@ Requires-Dist: requests-oauthlib ; extra == "standard" or extra == "oauth2" or e
56
60
  Requires-Dist: scikit-image ; extra == "test-images"
57
61
  Requires-Dist: sentry-sdk ; extra == "standard" or extra == "sentry" or extra == "all"
58
62
  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"
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"
62
66
  Project-URL: Repository, https://github.com/camptocamp/c2cwsgiutils
63
67
  Description-Content-Type: text/markdown
64
68
 
@@ -672,6 +676,64 @@ def hello_get(request):
672
676
  return {'hello': True}
673
677
  ```
674
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
+
675
737
  # Exception handling
676
738
 
677
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) # nosec
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)) # nosec
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 typing import TypeAlias
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( # pylint: disable=too-many-locals,too-many-statements
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]
@@ -129,7 +130,7 @@ def check_image( # pylint: disable=too-many-locals,too-many-statements
129
130
  if np.issubdtype(mask.dtype, np.floating):
130
131
  mask = (mask * 255).astype("uint8")
131
132
 
132
- assert ((0 < mask) & (mask < 255)).sum() == 0, "Mask should be only black and white image"
133
+ assert ((mask > 0) & (mask < 255)).sum() == 0, "Mask should be only black and white image"
133
134
 
134
135
  # Convert to boolean
135
136
  mask = mask == 0
@@ -146,7 +147,7 @@ def check_image( # pylint: disable=too-many-locals,too-many-statements
146
147
  return
147
148
  if not os.path.isfile(expected_filename):
148
149
  skimage.io.imsave(expected_filename, image_to_check)
149
- assert False, "Expected image not found: " + expected_filename
150
+ raise AssertionError("Expected image not found: " + expected_filename)
150
151
  expected = skimage.io.imread(expected_filename)
151
152
  assert expected is not None, "Wrong image: " + expected_filename
152
153
  expected = normalize_image(expected)
@@ -208,6 +209,7 @@ def check_screenshot(
208
209
  level: See `check_image`
209
210
  generate_expected_image: See `check_image`
210
211
  use_mask: See `check_image`
212
+
211
213
  """
212
214
  if headers is None:
213
215
  headers = {}