From 647c1922447b7287d0b7b2c665d694555eb00274 Mon Sep 17 00:00:00 2001 From: Dan Crosta Date: Sat, 17 Jun 2023 15:20:54 -0400 Subject: [PATCH] add support for pymongo 4.x --- docs/index.rst | 10 +++++++--- flask_pymongo/__init__.py | 27 ++++++++++++++++++++++++--- flask_pymongo/tests/test_gridfs.py | 24 +++++++++++++++++++++--- tox.ini | 5 ++--- 4 files changed, 54 insertions(+), 12 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index da62866..ace3eeb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -182,7 +182,7 @@ History and Contributors Changes: -- 2.4.0: Unreleased +- 3.0.0: Unreleased - `#125 `_ Drop MongoDB 3.2 support. @@ -191,8 +191,12 @@ Changes: - `#62 `_ Add support for :func:`~flask.json.jsonify()`. - `#131 `_ Drop - support for Flask 0.11 and Python 3.4; Add support for MongoDB 4.2, - PyMongo 3.9, and Flask 1.1. + support for Flask 0.11 and Python < 3.8; Change test matrix to test + latest MongoDB 4.x, 5.x, 6.x series + - Support conditional GET with explicit ETag value passed in to + ``save_file``; MongoDB has dropped support for automatic MD5 generation + in GridFS + - 2.3.0: April 24, 2019 diff --git a/flask_pymongo/__init__.py b/flask_pymongo/__init__.py index 5e0fc14..926abea 100644 --- a/flask_pymongo/__init__.py +++ b/flask_pymongo/__init__.py @@ -118,12 +118,15 @@ def init_app(self, app, uri=None, *args, **kwargs): app.json_encoder = self._json_encoder # view helpers - def send_file(self, filename, base="fs", version=-1, cache_for=31536000): + def send_file(self, filename, base="fs", version=-1, cache_for=31536000): # noqa: C901, E501 """Respond with a file from GridFS. Returns an instance of the :attr:`~flask.Flask.response_class` containing the named file, and implement conditional GET semantics (using :meth:`~werkzeug.wrappers.ETagResponseMixin.make_conditional`). + Conditional GET with ``If-Match`` is supported using the ``etag`` key + of the grid file's ``metadata`` dict, or the file's ``md5`` attribute, + if present. .. code-block:: python @@ -163,13 +166,24 @@ def get_upload(filename): ) response.content_length = fileobj.length response.last_modified = fileobj.upload_date - response.set_etag(fileobj.md5) + if fileobj.metadata and fileobj.metadata.get("etag"): + response.set_etag(fileobj.metadata["etag"]) + elif fileobj.md5: + response.set_etag(fileobj.md5) response.cache_control.max_age = cache_for response.cache_control.public = True response.make_conditional(request) return response - def save_file(self, filename, fileobj, base="fs", content_type=None, **kwargs): + def save_file( + self, + filename, + fileobj, + base="fs", + content_type=None, + etag=None, + **kwargs, + ): """Save a file-like object to GridFS using the given filename. .. code-block:: python @@ -185,6 +199,9 @@ def save_upload(filename): :param str content_type: the MIME content-type of the file. If ``None``, the content-type is guessed from the filename using :func:`~mimetypes.guess_type` + :param str etag: value to be used for HTTP entity tags in support + of conditional ``GET``. See `ETag on MDN + `_. :param kwargs: extra attributes to be stored in the file's document, passed directly to :meth:`gridfs.GridFS.put` """ @@ -196,6 +213,10 @@ def save_upload(filename): if content_type is None: content_type, _ = guess_type(filename) + if etag: + kwargs.setdefault("metadata", {}) + kwargs["metadata"]["etag"] = etag + storage = GridFS(self.db, base) id = storage.put(fileobj, filename=filename, content_type=content_type, **kwargs) return id diff --git a/flask_pymongo/tests/test_gridfs.py b/flask_pymongo/tests/test_gridfs.py index 5dc57da..1b5cddb 100644 --- a/flask_pymongo/tests/test_gridfs.py +++ b/flask_pymongo/tests/test_gridfs.py @@ -63,7 +63,8 @@ def setUp(self): # make it bigger than 1 gridfs chunk self.myfile = BytesIO(b"a" * 500 * 1024) - self.mongo.save_file("myfile.txt", self.myfile) + self.etag = md5(self.myfile.getvalue()).hexdigest() + self.mongo.save_file("myfile.txt", self.myfile, etag=self.etag) def test_it_404s_for_missing_files(self): with pytest.raises(NotFound): @@ -77,12 +78,12 @@ def test_it_sets_content_length(self): resp = self.mongo.send_file("myfile.txt") assert resp.content_length == len(self.myfile.getvalue()) - def test_it_sets_supports_conditional_gets(self): + def test_it_sets_supports_conditional_gets_from_etag(self): # a basic conditional GET environ_args = { "method": "GET", "headers": { - "If-None-Match": md5(self.myfile.getvalue()).hexdigest(), + "If-None-Match": self.etag, }, } @@ -90,6 +91,23 @@ def test_it_sets_supports_conditional_gets(self): resp = self.mongo.send_file("myfile.txt") assert resp.status_code == 304 + def test_it_sets_supports_conditional_gets_from_md5(self): + myfile = BytesIO(b"hello world") + etag = md5(myfile.getvalue()).hexdigest() + self.mongo.save_file("myfile-md5.txt", myfile, md5=etag) + + # a basic conditional GET + environ_args = { + "method": "GET", + "headers": { + "If-None-Match": etag, + }, + } + + with self.app.test_request_context(**environ_args): + resp = self.mongo.send_file("myfile-md5.txt") + assert resp.status_code == 304 + def test_it_sets_cache_headers(self): resp = self.mongo.send_file("myfile.txt", cache_for=60) assert resp.cache_control.max_age == 60 diff --git a/tox.ini b/tox.ini index 8aab5d3..ad13ae0 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,7 @@ [tox] -; pymongo{311,312,40,41,42,43}-mongo{4x,5x,6x}-flask{10,11,2x}, style envlist= - pymongo{311,312}-mongo{4x,5x,6x}-flask{10,11,2x}, style + pymongo{311,312,40,41,42,43}-mongo{4x,5x,6x}-flask{10,11,2x}, style [testenv] docker = @@ -28,7 +27,7 @@ deps = flask2x: flask>=2.0,<3.0 commands = - {envbindir}/py.test --tb=native {toxinidir} + py.test --tb=native {toxinidir}/flask_pymongo {posargs} [testenv:style] skipsdist = true